Bypass the Docker Firewall by Abusing REST API

Learn about the misconfigurations in the Docker API firewall and how to take advantage of them to break into a container.

Bypass the Docker Firewall by Abusing REST API
Photo by Jason Hudson / Unsplash

Hello World! Once you get a foothold on the machine that has access to docker via Unix socket or TCP connection, it is damn easy to get access to the host machine by mounting its file system in the container and running that container. Keeping that in the mind, security teams often implement the API firewall that sits in between the docker engine and the API server. This is responsible to filter the user inputs and pass on only the allowed ones.

In this post, I will be discussing the following labs from the AttackDefense platform.

If you want to practice the labs, I have another two of them for you. To solve these labs, you can take references from Container Breakout (Part 1 and Part 2).

Unchecked JSON Structure

In this lab, you are provided with the docker CLI and a low privileged shell. To prevent direct escalation to the root user, bind mounts are disabled for the low privileged user, as you can see in the following screenshot.

Not only the bind mount but also spawning privileged containers are disabled. This is because, in the case of privileged containers, the filesystems of the host will be references in the /dev and can be mounted.

Privileged actions are not allowed

The docker CLI depends on the UNIX socket by default but sends the default JSON schema which is then audited by the API. Luckily after searching, I found that it is possible to execute curl requests with a Unix socket. Read more.

Sometime http:///request/path doesn't work with curl. In that case, you can use any hostname like http://local/request/path. This will be only used to successfully parse the URI string and curl will use the host and port details from the Unix socket instead.

Test docker API interaction with the Unix docker

The docker engine accepts the requests in the form of the JSON object which is then parsed and validated by the API. So basically it will reject all the privileged properties in the HostConfig field like Binds, Privileged and CapAdd and Capabilities.

In the lab description, it says that a firewall can be bypassed by sending different JSON structures. This means there has to be some different JSON payload supported by the engine but is ignored (not checked) by the firewall.

After trying certain naive JSON payloads in the HostConfig, I got no positive response from the endpoint. So I looked at the lab handbook to see what the actual payload is.

In this, they are using Binds outside the HostConfig attribute and in the manual, it is working. So the following is the curl request that does the same. In this payload Image is used to tell the engine which base image should be used to create a container, Entrypoint is used to override the default value from the ENTRYPOINT in the image and you already know what Binds will do.

curl --location --request POST 'http://localhost/containers/create' \
--unix /var/run/docker.sock \
--header 'Content-Type: application/json' \
--data-raw '{
    "Image": "alpine:latest",
    "Binds": [
        "/:/host"
    ],
    "Entrypoint": ["tail", "-f", "/dev/null"]
}'
Curl command to create the container

This time executing the curl command will succeed resulting in content creation. Recall that docker run is the combination of two docker commands docker container create and then docker container start.

You can confirm this by executing docker ps with -a flag to all the containers, whether they are created, running or stopped.

Container with configuration in curl command created
Additionally to verify the Binds configuration you can use docker inspect <container> command. This will give you all the details of the container and you will also find the bind mounts information in it.

Now start the container and immediately get the /bin/sh shell using docker exec command on the same container. You will see the /host directory

Start the container and read the flag file

When I asked the AttackDefense about why Binds field is supported outside HostConfig object, I receive the following response, which is hard for me to believe.

I tried searching google, watched a few videos from the docker conferences and also searched this in the reference of the docker API (in all the versions available). If you find any concrete reasoning for this, please do let me know.

Exploiting Protected Docker API

In this lab, you are provided with a target server running a vulnerable web application on a 10000 port and the docker TCP service on the default port. Did you notice the question mark sign ? besides the docker service? What is that? The STATE is open which means that nmap has recognized the service from the nmap-services list, but could not confirm it [read more].

Get the list of the open ports

This will make sense after seeing the curl request below. This is blocked by the firewall which is looking for Bypass-Token in the header or in the environment variables.

The nmap service detector function was unable to confirm the docker service because of this unsuccessful response. It returned the service from the heuristics with the assumption that the default service would be running on port 2375.

Curl failed to retrieve the docker engine version

It is dead-end for now. But there is another service running Wolf CMS and you are provided with the login credentials in the lab description. Enter the credentials on the form below.

The admin login page in Wolf CMS can be found at /?/admin/login request path
Login to the Wolf CMS

There is a vulnerability in the current version which can allow you to upload any file and which can be then executed/viewed from /public/<filename> request path.

Create a simple php script to dump all the environments using printenv shell command. To execute the system command, you can use system() function in the php.

<?php system("printenv"); ?>

Once the file is uploaded, execute it to retrieve the environment variables. You will find the Bypass-Token environment value which is now required to authenticate the curl requests.

Dump the Bypass-Token from the environment variable

This was the first guess that came out of intuition after reading a message from the error in the above curl request. While spawning the docker container, -e flag is used to set the environment variable for that container which is then inherited by all the processes.

Now if you would try the same curl request endpoint (/version), it will work and return version information as you do docker --version.

Curl request with Bypass-Token works
💡
Alternatively, you can also use docker cli by setting custom HttpHeaders in the docker config file. The default storage of this file is in $HOME/.docker or you can use --config option in the docker CLI.

In this post, I will stick to curl, because it is cool and to learn docker-engine interaction with it.

Now is the time to list the images stored on the host system which will be required to start the container. This can be done by calling /images/json endpoint.

The list already pulled images

Create a container with Binds information to mount the host filesystem at /host and Detach set it to true using the wolfcms:latest image. It will start in the background and run the container. This would be the equivalent command: docker container create -v /:/host -d.

Create a container with worlfcms:latest image

To start the container, execute the POST request at /containers/<ID>/start endpoint. I trust the docker api, but if you don't, you can try checking the status of the containers using /containers/json endpoint.

Start the container

When you run the docker exec command on the terminal, under the hood it creates and then starts the exec session and attaches it to your console. This is how you will now get the shell access from the remote container.

By sending a POST request to the /containers/<ID>/exec endpoint, you can create an exec session. Standard i/o are only required for the interactive sessions, not to obtain a reverse shell. Provide the reverse shell command in the Cmd field.

I have copied this from the pentestermonkey reverse shell cheatsheet.
Create exec session to get a reverse connection via bash<

Once the exec session is created, it is not started automatically. You must execute the POST request on the /exec/<EXEC ID>/start endpoint.

Note: Before starting the exec session, remember to start the netcat listener. I made that mistake the first time, so I'll have to redo the exec.

As soon as you will get the reverse connection, chroot in the /host directory and retrieve the flag file.

Successfully access host filesystem and the retrieved flag file

References