作者:HuanGMz@知道创宇404实验室
时间:2022年6月7日

English version: https://paper.seebug.org/1914/

分析一下最近Microsoft Office 相关的 MSDT 漏洞。

1. WTP 框架

文档:https://docs.microsoft.com/en-us/previous-versions/windows/desktop/wintt/windows-troubleshooting-toolkit-portal

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 框架提供了一种自动化 检测/修复 故障的方式。

WTP 结构:

wtp架构

上图展示了 WTP 的底层结构:

  • WTP由两个进程组成,Process1 是带UI 的 Troubleshooting Run-time Engine,Process2 用于提供 Windows PowerShell Runtime 环境。
  • Process2 提供的 PowerShell 运行时环境提供了4条特殊的 PowerShell 命令:Get-DiagInput, Update-DiagReport, Update-DiagRootCause, Write-DiagProgress。
  • Troubleshooting Pack 运行在Process1 和 Process2 所构建的平台上。

故障排除包 是用户可编程部分,其本质上是一组 针对特定故障的 检测/修复脚本。Process1 的Troubleshooting Run-time Engine 从故障排除包中获取 检测脚本,并交给Process2 运行。Process2 中特殊的 PowerShell 运行时环境提供了4条专用的命令给故障排除包里的脚本使用。

故障排除包的设计基于三个步骤:检测问题(troubleshooting)、解决问题(resolution)和验证解决方案(verification),对应 TS_、RS_、VF_ 三种脚本。

实际上 Process1 就是 msdt.exe ,Process2 则是 sdiagnhost.exe。sdiagnhost.exe 为了给msdt.exe 提供运行脚本的能力,注册了IScriptedDiagnosticHost com接口,相应的com方法就是:RunScript()。

WTP 还提供了一系列的默认故障排除包,可以在 ms-msdt 协议里通过 -id 参数指定。本次漏洞中所使用的 PCWDiagnostic 就是其中之一,用于程序兼容性的故障排除。

2. 漏洞复现与调试方法

漏洞复现:

该漏洞可通过 doc 或 rtf 文档的形式触发,但为了调试方便,我们直接使用 msdt.exe 命令触发:

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 "

cmd 中使用上面的命令触发漏洞(不要直接用powershell)。

漏洞调试:

mspaint.exe 进程创建在 sdiagnhost.exe 下,且 PowerShell Runtime 由c# 实现。尽管 sdiagnhost.exe 本身是一个非托管程序,我们仍然可以使用 dnspy 来进行 .net 调试。

设置好 dnspy 调试所需要的环境变量:

COMPlus_ZapDisable=1
COMPlus_ReadyToRun=0

在 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ 注册表路径下创建 sdiagnhost.exe 项,并在该项下创建 Debugger 字符串,值为dnspy 路径。

使用前面的命令触发漏洞,然后会看到 dnspy 被调用,但处于未开始调试的状态。此时需要我们手动点击上面的"启动" 来开启调试。

image-20220607003012247

在 Microsoft.Windows.Diagnosis.SDHost.dll 里的 Microsoft.Windows.Diagnosis.ManagedHost.RunScript() 方法下断点。该方法实现了 IScriptedDiagnosticHost com接口里的 RunScript() 方法,用于给 msdt.exe 提供执行检测脚本所需的 PowerShell 运行时环境。然后重新触发漏洞,便可在此中断。

image-20220607003519577

image-20220607003529665

RunScript() 方法一共被触发了两次,第一次用于调用 TS 脚本,第二次用于调用 RS 脚本,且第二次有参数。

3. 漏洞原因与触发条件

本质上这是一个 PowerShell 代码注入漏洞。

image-20220607003950016

ManagedHost.RunScript() 使用 PowerShell.AddScript() 方法来添加要执行的命令,并且text 中的部分内容可控(参数部分)。这是典型的 PowerShell 代码注入漏洞,使用AddScript() 会导致在调用时对 text 里的 $ 字符进行语法解析(优先将其解析为子表达式运算符)。

类似于下面这样:

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

实际上漏洞触发于第二次调用 RunScript(),调用 RS 脚本时,相应的 text 为:

@"& ""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"""

可以看到传给 AddScript() 的字符串没有对 $ 符号进行过滤,导致了代码注入。

触发条件:

要想成功触发对 RS_ProgramCompatibilityWizard.ps1 的调用,要先通过 TS_ProgramCompatibilityWizard.ps1 脚本的检测。

观察TS_ProgramCompatibilityWizard.ps1 脚本的代码:

image-20220607010113704

Get-DiagInput 命令就是我们前面提到的WTP PowerShell Runtime 提供的4条特殊命令之一,该命令用于从用户获取输入信息。这里获取我们传入的 IT_BrowseForFile 参数 并赋值给了 $selectedProgram 变量。

而后调用 Test-Selection方法来对 $selectedProgram 进行检测:

image-20220607011215797

该函数首先使用 test-path 命令来对路径进行检测,以保证路径存在。然后要求路径的扩展名为 exe 或 msi。

但是 test-path 对于使用 /../ 返回到根路径之外的路径会返回True,比如下面的:

image-20220607012014662

这里以 \ 开头,表示当前盘符的根目录,\..\ 存在,所以 \..\..\ 便超出了范围,返回为true。也可以像c:\..\..\hello.exe 这样。如果像原始payload 那样以一个普通字符开头,考虑到 TS_ProgramCompatibilityWizard.ps1 脚本所在的临时目录,以及 该普通字符所占一级,至少需要9个 \..\ 才可以。

image-20220607013504806

然后从 $selectedProgram 里提取文件名,并过滤$符号,以防代码注入。但这行代码其实是错的,正确的写法如下:

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

由于原来的脚本中直接使用 "$",该 "$" 实际会在传给Replace之前被PowerShell 引擎解析,根本无法匹配到 $ 字符。

image-20220607013238153

TS 脚本的最后,使用了Update-DiagRootCause 命令,该命令也是4条特殊命令之一,用于报告root cause 的状态。注释中写道该命令会触发调用 RS_ 脚本,-parameter 指定的字典会被作为参数传给脚本。导致第二次调用 RunScript() 方法,并且参数中的 -TargetPath 可控,进而触发了漏洞。

参考资料:

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/wintt/windows-troubleshooting-toolkit-portal


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1913/