背景
随着现代 VPN 或 SASE 解决方案的采用,始终在线或长期存在的 VPN 会话成为越来越常见的配置。更少的身份验证意味着更少的用户摩擦,并且始终在线的解决方案可确保对来自端点的网络流量的一致可见性。如果做得正确,这就是双赢。
始终在线或长期存在的 VPN 配置意味着设备必须存储一些身份验证材料(即 cookie),VPN 客户端使用该材料来恢复连接而无需用户交互。与任何其他 cookie 或凭证材料一样,这为对手提供了窃取和重放以获得访问权限的机会。在这篇文章中,我们将调查这样一个产品(Palo Alto 的 GlobalProtect 客户端)如何做出合理但最终失败的努力来保护此类凭证材料。
寻找Cookie
像所有优秀的研究工作一样,我们从 Google 和 RTFM 开始。从Palo Alto 官方文档中,我们了解到 GlobalProtect 客户端将其 cookie(称为“门户用户身份验证 Cookie”)存储在%LOCALAPPDATA%
的文件中。具体来说: %LOCALAPPDATA%Palo Alto NetworksGlobalProtectPanPUAC_<hash>.dat
在十六进制编辑器中快速查看此文件会发现该文件受 DPAPI 保护,如其独特的标头所示, 01 00 00 00 D0 8C 9D DF 01 15 D1 11 8C 7A 00 C0 ...
DPAPI 受保护的 Cookie
对此文件调用CryptUnprotectData
会产生另一个无法使用的高熵(可能已加密)数据块。
DPAPI 未受保护的 Cookie 文件的熵
由此,我们可以做出两个假设:
-
这些 .dat 文件可能在存储之前以某种方式加密和/或序列化。
-
VPN 客户端使用一些硬编码或派生密钥材料对其进行解密。
逆转策略
作为一名相当缺乏经验的逆向工程师,深入研究闭源软件中使用的加密实现的想法感觉有点令人畏惧。我可能做了一些困难的事情,但最终有帮助的是:
-
IDA 和 Ghidra 显然不会以可读源进行 1:1 反编译,因此我使用动态分析来帮助更好地理解某些函数。当用动态分析补充静态分析时,禁用ASLR。在 x64dbg 中附加到进程后,找到映像的基地址并在 IDA 中相应地重新设置程序的基址。这使得在调试器中要跟踪的函数上设置断点变得更加容易。
-
详细记录 FTW!值得庆幸的是,GlobalProtect 具有非常详细的日志记录,我们可以使用它来更轻松地查找和跟踪相关函数。在 IDA 中搜索调试字符串,找到外部参照,然后从那里向后工作。这正是我在下面所做的。
当我们调查客户端如何管理存储的 cookie 时,我首先在 IDA 中搜索任何包含“cookie”或“decrypt”的字符串。这产生了几个结果,其中有两个很突出:一个指向一个名为UnserializePortalAuthCookie
的函数,另一个与文件解密相关。
反编译代码表明存在 UnserializePortalAuthCookie 函数
在同一个函数中,我们看到在检查另一个函数调用的结果后发生的日志记录,表明该函数可能负责解密 cookie 文件,我在 IDA 中相应地重命名了该文件。
反编译代码表明存在文件解密功能
识别 AES 密码并恢复密钥
在解密函数中,我们看到对EVP_EncryptInit_ex
以及后续EVP_DecryptUpdate
和EVP_DecryptFinal_Ex
调用的字符串/调试日志引用。Google 搜索这些字符串将我们指向 OpenSSL 库,这暗示我们 OpenSSL 可能静态链接到服务二进制文件。我们可以通过深入研究一些功能并将它们与开源进行比较来确认这一点。例如, EVP_EncryptInit_ex
函数的反编译几乎与源函数相同,证实了我们的怀疑:
OpenSSL EVP_DecryptInit_Ex 与反编译代码的比较
查看 EVP 函数调用的整体流程(与公开示例相比)表明可能正在使用 AES 加密密码。值得注意的是, EVP_EncryptInit_ex
和EVP_DecryptInit_ex
使用密钥和 IV 进行初始化,因此我在这些函数地址上设置断点以转储这些值。
在 EVP_DecryptInit_Ex 调用中恢复密钥和 IV
通过这个,我们恢复了一个可能的密钥, C4 10 06 BC DB EF 66 83 B2 E7 38 7E A9 48 7A 77 C4 10 06 BC DB EF 66 83 B2 E7 38 7E A9 48 7A 77
以及看似无效的 IV。值得注意的是,密钥是重复的 16 字节模式。
此时我只是猜测它是 AES-256-CBC(基于密钥长度),结果证明是正确的。使用Cyberchef进行测试,我们确认门户配置文件成功解密。
GlobalProtect 客户端门户配置文件的解密标头
回顾一下我们所知道的和需要进一步调查的内容:
-
OpenSSL 库用于加密和解密。
-
存储的 .dat 文件受 DPAPI 保护,并使用一致密钥和空 IV 进行 AES-256-CBC 加密。
-
AES-256 密钥是重复的 16 字节模式,在加密/解密功能完成后不会出现在内存中,并且不会出现在二进制文件中(即,这不是硬编码密钥。)
在这种情况下,我们应该假设存在一些我们现在需要找到并重现的密钥导出函数。
研究密钥派生
重新审视解密函数时,我们看到在解密之前调用了另一个函数,该函数通过一系列嵌套函数执行一些复杂的字节操作。这些函数是迟钝的且不易于阅读,但幸运的是,其中一个函数的反编译代码提供了非常强烈的暗示,说明这里可能发生的情况:
void __fastcall z_unknown_1(_DWORD *a1)
{
a1[1] = 0;
*a1 = 0;
a1[2] = 1732584193;
a1[3] = -271733879;
a1[4] = -1732584194;
a1[5] = 271733878;
}
快速 Google 搜索确认这些值对应于 MD5 状态变量。回想一下我们之前观察到的 AES 密钥,MD5 确实有意义,因为它会生成一个 16 字节的值。再次查看 C/C++ 中 MD5 哈希的公共代码示例,我们将这些函数标识为MD5_Init
、 MD5_Update
和MD5_Final
。有了这些信息,两个关键函数就可以重命名如下:
_BYTE *__fastcall z_get_pan_md5(_BYTE *out_buf)
{
int md5_state[24]; // [rsp+20h] [rbp-29h] BYREF
int pann_str[4]; // [rsp+80h] [rbp+37h] BYREF
qmemcpy(pann_str, "pannetwork", 10);
z_md5_init(md5_state);
z_md5_update((char *)md5_state, (char *)pann_str, 10);
z_md5_final(md5_state, out_buf);
return out_buf;
}
_BYTE *__fastcall z_get_md5_key(char *data_ptr, int data_len, _BYTE *out_buf)
{
char *pan_md5; // rax
int md5_state[24]; // [rsp+20h] [rbp-88h] BYREF
z_md5_init(md5_state);
z_md5_update(md5_state, data_ptr, data_len);
pan_md5 = z_get_pan_md5(out_buf);
z_md5_update(md5_state, pan_md5, 16);
z_md5_final(md5_state, out_buf);
return out_buf;
}
那么,这里发生了什么?该代码嵌套 MD5 哈希,其中传递给z_get_md5_key
函数的数据与硬编码字符串pannetwork
的 MD5 哈希连接,然后使用 MD5 再次进行哈希处理。本质上,它是在执行 MD5(input + MD5(“pannetwork”))
。为了确定哪些数据被传递到这个哈希函数中,我在z_get_md5_key
上设置了一个断点,并检查了 RCX 和 RDX 寄存器,它们分别保存数据指针和数据长度。
计算机 SID 传递给 MD5 哈希函数
在RDX寄存器中我们看到数据长度是0x18(24字节),传递给函数的24字节字符串是 01 04 00 00 00 00 00 05 15 00 00 00 EF C8 89 7F 22 AF 1E 09 04 2D C8 51
……
在 IDA 中再次回溯调用,我们发现传递给z_get_md5_key
指针最终来自一个使用 Win32 API 调用检索计算机 SID 的函数( GetComputerNameExW
-> LookupAccountNameW
-> LookupAccountNameW
-> ConvertSidToStringSidA
),以十六进制形式返回计算机 SID表示。如果这些调用中的任何一个失败,该函数将诉诸使用硬编码字符串global135protect
。
使用 python 我们可以确认这实际上是密钥导出函数:
import hashlib
sidbytes = bytearray([0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x15, 0x00, 0x00, 0x00, 0xEF, 0xC8, 0x89, 0x7F,
0x22, 0xAF, 0x1E, 0x09, 0x04, 0x2D, 0xC8, 0x51])
pannetwork_str = "pannetwork"
md5_pannetwork = hashlib.md5(pannetwork_str.encode()).digest()
finalbytes = sidbytes + md5_pannetwork
finalmd5 = hashlib.md5(finalbytes).hexdigest()
print(finalmd5)
c41006bcdbef6683b2e7387ea9487a77
这确实产生了我们在 AES 密钥中看到两次重复的 16 字节值!所以,我们最终的 KDF 是:
MD5(ComputerSID + MD5("pannetwork")) + MD5(ComputerSID + MD5("pannetwork"))
现在可能是分享的好时机,我不是第一个发现这一点的人。在从事该项目时,一位同事分享了 Crowdstrike 研究团队的一篇文章,记录了这个_exact_ 密钥派生函数,但在客户端更新过程中的 EoP 漏洞利用的上下文中使用。
https://www.crowdstrike.com/blog/exploiting-escalation-of-privileges-via-globalprotect-part-1/
传递 Cookie
有了解密的会话 cookie,我们就可以使用完全实现 GlobalProtect 协议的“openconnect”客户端,包括使用门户身份验证 cookie 进行身份验证。在提示时提供恢复的 cookie 后,这将向门户验证我们的身份,并提示我们选择要连接的网关(取决于配置,因为某些部署使用门户作为网关)。
$ sudo openconnect --protocol=gp --user="example\username" --usergroup=portal:portal-userauthcookie --os=win https://vpn.example.com
就这样,我们就上线了!除了…。
主机信息配置文件检查
对 VPN 进行身份验证是第一步,但成熟的部署应该使用 GlobalProtect 的主机信息配置文件 (HIP) 检查来根据一组合规性策略来检查主机。现有的规避这些检查的研究已经很成熟,但有了解密数据文件的能力,我们可以更进一步。GlobalProtect 将主机信息配置文件报告的信息存储在 %PROGRAMFILES%Palo Alto NetworksGlobalProtect
:
| File | Purpose |
| --------------------- | ---------------------------------------------------------------------------- |
| HipPolicy.dat | All configured HIP checks performed by the agent (does not contain results). |
| HIP_AM_Report_V4.dat | Anti-malware policy check results. |
| HIP_BC_Report_V4.dat | Backup compliance check results. |
| HIP_DE_Report_V4.dat | Disk encryption check results. |
| HIP_DLP_Report_V4.dat | DLP check results. |
| HIP_FW_Report_V4.dat | Host based firewall check results. |
| HIP_PM_Report_V4 | Patch management check results. |
| PanGPS.log | PanGPS service log (may contain HIP related data). |
| PanGPHip.log | HIP specific log, usually contains full HIP XML profile. |
所有 .dat 文件均使用相同密钥进行 AES 加密,因此我们可以在兼容的主机上轻松解密这些文件并使用它们重建工作配置文件。我们还可以使用日志文件内容来拼凑出工作配置文件,因为在某些情况下,整个 HIP 配置文件 XML 内容都会转储到PanGPHip.log
文件中。
幸运的是, openconnect
支持通过--csd-wrapper
参数从 shell 脚本传递 HIP 报告数据。相当简单,我们可以获取从日志中恢复的 XML/从上面的 .dat 文件构建的 XML,并替换 openconnect 项目中 hipreport.sh 脚本的相关部分。例如,如果合规性策略需要实时 Windows Defender 扫描和最近扫描,我们可能包括以下内容:
<entry name="anti-malware">
<list>
<entry>
<ProductInfo>
<Prod vendor="Microsoft Corporation" name="Windows Defender" version="4.18.2304.8" defver="1.389.187.0" engver="1.1.20300.3" datemon="5" dateday="4" dateyear="2023" prodType="3" osType="1"/>
<real-time-protection>yes</real-time-protection>
<last-full-scan-time>$NOW</last-full-scan-time>
</ProductInfo>
</entry>
</list>
</entry>
我们可以在连接设置期间传递它,如下所示:
$ sudo openconnect --protocol=gp --user="example\username" --usergroup=portal:portal-userauthcookie --os=win https://vpn.example.com --csd-wrapper ~/tools/custom-hips-profile.sh
实施
随着 EDR 能力的不断提高,红队始终面临着寻找新方法来在端点上保持规避的挑战。这导致“远离陆地”或“远离外国土地生活”的贸易技术得到更广泛的采用,通过将我们的工具穿过植入物来减少我们在端点上的足迹。在某些情况下,这是不切实际的,甚至通过我们的 C2 本地 SOCKS 通道传输标准攻击工具(例如 impacket)也可能面临被一些领先的 EDR 产品检测到的风险。新颖的 Active Directory 交易技术(例如 SpecterOps 引入的无数 NTLM 中继和对象接管原语)通过植入执行也可能非常繁琐,因为它需要将多个端口转发链接在一起,并且在某些情况下需要绑定访问端口 445,这是在非特权上下文中无法访问。
将我们的红队控制设备连接到 VPN 可以解决其中许多挑战,并且使用已建立的 VPN 会话 cookie 使我们无需用户凭据或 MFA 即可实现此目的。
用于解密和收集 Windows GlobalProtect 安装中所有相关文件的 PoC 工具将在本博客文章发布后一段时间提供,并在同一存储库中立即提供 Sigma 和 Yara 规则形式的对策。
https://github.com/rotarydrone/GlobalUnProtect
防御指导
除了存储库中专门对 GlobalUnProtect 和 OpenConnect 工具进行签名的对策外,防御者还可以考虑以下措施:
-
对来自多个位置的多个活动 VPN 连接的用户发出警报
-
使用 HIP 配置文件阻止来自非托管设备的连接
-
针对 HIP 故障和不合规设备发出警报
-
深入防御和检测
原文始发于微信公众号(合规渗透):解密和重放 VPN Cookie 以实现对VPN的渗透
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论