Author: HuanGMz@Knownsec 404 Team
Chinese version:

1. WTP


Windows Troubleshooting Platform (WTP) provides ISVs, OEMs, and administrators the ability to write troubleshooting packs that are used to discover and resolve issues found on the computer

WTP provides an automated way to troubleshoot/fix.

WTP architecture:


The above figure shows the underlying structure of WTP:

  • WTP consists of two processes, Process 1 is the Troubleshooting Run-time Engine with UI and Process 2 is used to provide Windows PowerShell Runtime environment.
  • The PowerShell runtime environment provided by Process 2 provides 4 special PowerShell commands: Get-DiagInput, Update-DiagReport, Update-DiagRootCause, Write-DiagProgress.
  • The Troubleshooting Pack runs on the platforms Process 1 and Process 2 are built on.

Troubleshooting packages are user-programmable parts, which are essentially a set of detection/fix scripts for specific faults. Process1's Troubleshooting Run-time Engine takes the troubleshooting scripts from the troubleshooting package and gives them to Process2 to run. The special PowerShell runtime environment in Process2 provides 4 dedicated commands for use by scripts in the troubleshooting package.

The design of the troubleshooting package is based on three steps: troubleshooting, resolution and verification, corresponding to three scripts: TS_, RS_, VF_.

Actually Process 1 is msdt.exe and Process 2 is sdiagnhost.exe. In order to provide msdt.exe with the ability to run scripts, sdiagnhost.exe registers the IScriptedDiagnosticHost com interface, and the corresponding com method is: RunScript().

WTP also provides a set of default troubleshooting packages that can be specified via the -id parameter in the ms-msdt protocol. PCWDiagnostic used in this vulnerability is one of them, for troubleshooting program compatibility.

2. Vulnerability recurrence and debugging methods

Vulnerability recurrence :

The vulnerability can be triggered in the form of doc or rtf document, but for the convenience of debugging, we directly use the msdt.exe command to trigger:

C:\Windows\system32\msdt.exe ms-msdt:/id PCWDiagnostic /skip force /param "IT_RebrowseForFile=cal?c IT_SelectProgram=NotListed IT_BrowseForFile=fff$(IEX('mspaint.exe'))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe "

Use the above command in cmd to trigger the exploit (don't use powershell directly).

Vulnerability debugging:

The mspaint.exe process is created under sdiagnhost.exe, and the PowerShell Runtime is implemented by c#. Although sdiagnhost.exe is an unmanaged program, we can still use dnspy for .net debugging.

Set the environment variables required for dnspy debugging:


Create a sdiagnhost.exe key under the "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\" registry path, and create a Debugger string under this key with the value of the dnspy path.

Use the above command to trigger the exploit. Then you will see dnspy called, but in a state where debugging has not started. At this point, we need to manually click "Start" above to start debugging.


Set a breakpoint in the Microsoft.Windows.Diagnosis.ManagedHost.RunScript() method in Microsoft.Windows.Diagnosis.SDHost.dll. This method implements the RunScript() method in the IScriptedDiagnosticHost com interface and is used to provide msdt.exe with the PowerShell runtime environment required to execute the troubleshooting script. Then retrigger the vulnerability to break here.



The RunScript() method is triggered twice, the first time is used to call the TS script, the second time is used to call the RS script, and the second time has parameters.

3.Vulnerability causes and trigger conditions

Essentially this is a PowerShell code injection vulnerability.


ManagedHost.RunScript() uses the PowerShell.AddScript() method to add commands to be executed, and part of the text is controllable (parameters part). This is a classic PowerShell code injection vulnerability. Using AddScript() will cause the $ character in text to be parsed (preferably as a subexpression operator) when called.

Something like this:

PowerShell powerShellCommand = PowerShell.Create();
powerShellCommand.AddScript("ls -test $(iex('mspaint.exe'))");
var result = powerShellCommand.Invoke();

In fact the vulnerability is triggered by the second call to RunScript(). The corresponding text is:

@"& ""C:\Users\MRF897~1.WAN\AppData\Local\Temp\SDIAG_d89d16cb-49d3-48ef-bea4-daebc1919abb\RS_ProgramCompatibilityWizard.ps1"" -TargetPath ""h$(IEX('mspaint.exe'))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe"" -AppName ""mpsigstub"""

We can see that the string passed to AddScript() is not filtered for the $ symbol, which leads to code injection.

Triggering conditions:

In order to successfully trigger the call to RS_ProgramCompatibilityWizard.ps1, it must first pass the detection of the TS_ProgramCompatibilityWizard.ps1 script.

Observe the code of the TS_ProgramCompatibilityWizard.ps1 script:


The Get-DiagInput command is one of the four special commands provided by the WTP PowerShell Runtime we mentioned earlier, which is used to obtain input information from the user. Here we get the IT_BrowseForFile parameter we passed in and assign it to the $selectedProgram variable.

Then call the Test-Selection method to test $selectedProgram:


The function first uses the test-path command to detect the path to ensure that the path exists. Then the extension of the path is required to be exe or msi.

But test-path will return True for paths beyond the root using \..\, such as the following:


Here it starts with \, which means the root directory of the current drive letter. \..\ exists, so \..\..\ is out of scope and returns true. Like c:\..\..\hello.exe. If it starts with a normal character like the original payload, considering the temporary directory where the TS_ProgramCompatibilityWizard.ps1 script is located, and the normal character occupies one level, at least 9 \..\ are required.


Then extract the filename from $selectedProgram and filter the $ sign to prevent code injection. But this line of code is actually wrong. The correct way to write it is as follows:

$appName = [System.IO.Path]::GetFileNameWithoutExtension($selectedProgram).Replace("`$", "``$")

Since the original script uses "$" directly, the "$" will actually be parsed by the PowerShell engine before being passed to Replace. So it can't match out the $ character.


At the end of the TS script, the Update-DiagRootCause command is used, which is also one of the 4 special commands used to report the status of the root cause. The comment says that this command will trigger the invocation of the RS_ script, and the dictionary specified by -parameter will be passed to the script as a parameter. This causes the RunScript() method to be called a second time, and the -TargetPath in the parameter is controllable, which triggers the vulnerability.


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址: