Citrix CVE-2022-27518 漏洞分析
2023-2-17 11:13:0 Author: paper.seebug.org(查看原文) 阅读量:47 收藏

作者:[email protected]知道创宇404实验室
日期:2023年2月17日

漏洞介绍

Citrix在2022年12月份发布了CVSS评分9.8的CVE-2022-27518远程代码执行漏洞通告,距今已经过去两个多月了,由于漏洞环境搭建较为复杂,一直没有相关的分析文章。经过一段时间的diff分析及验证后,发现漏洞成因在于Citrix netscaler在解析SAML xml时对SignatureValue字段校验不严格导致了栈溢出。

漏洞影响版本:

  • Citrix ADC and Citrix Gateway 13.0 before 13.0-58.32

  • Citrix ADC and Citrix Gateway 12.1 before 12.1-65.25

  • Citrix ADC 12.1-FIPS before 12.1-55.291

  • Citrix ADC 12.1-NDcPP before 12.1-55.291

CVE-ID Description CWE Affected Products Pre-conditions
CVE-2022-27518 Unauthenticated remote arbitrary code execution CWE-664: Improper Control of a Resource Through its Lifetime Citrix Gateway, Citrix ADC Citrix ADC or Citrix Gateway must be configured as a SAML SP or a SAML IdP

根据披露信息,只有当ADC或者Gateway配置为SAML SP(资源提供方服务器)或者SAML IdP(身份认证服务器)时,才会受到漏洞影响。

不熟悉SAML协议流程的可以参考[1][2],本文不再详细阐述,基本的认证流程如下:

漏洞环境搭建

漏洞环境搭建非常复杂(甚至比漏洞分析分析耗时久:( ),在查阅大量资料后,我使用Citrix Gateway作为SAML SP,使用Microsoft Azure作为SAML IDP(可能需要高级账号)构建了SAML单点登录环境。如果使用虚拟机搭建Citrix SAML服务,需要三台虚拟机,同时比较麻烦的一点是,Citrix的SAML服务只有铂金版、企业版才能提供,因此需要相应的高级版本激活码,可以去闲鱼上找一找,好在404师傅们直接把激活流程给hack了(Orz。

具体搭建过程可以参考后面的文章。

配置详情

配置后的网络拓补图如下[3],三台虚拟机在同一内网环境中,分别对应NSIP、MIP(SNIP)、VIP。其中NSIP是Citrix ADC/Gateway设备的自身IP,用于管理、对NetScaler自身进行常规访问以及在高可用性配置中实现设备间通信的IP地址;MIP是映射IP,是设备向后端真实服务器发送请求包中的源地址。VIP是虚拟服务器IP,客户对可以对其直接进行访问,真正响应的请求是其后端的众多真实服务器。管理多种流量的一个设备可配置有多个VIP。

还需要一台域控服务器用来给Citrix服务器发放证书。(理论上来说自签名证书也可以,我直接构建了一个DNS通配符证书,可以参考Create a Wildcard Certificate using MMC in Windows Server 2019 - YouTube

在我的环境中配置清单如下:

IP地址 域名 用途
Citrix Gateway13.0-52.24 10.0.25.171 NSIP
Citrix Gateway13.0-52.24 10.0.25.172 MIP
Citrix Gateway13.0-52.24 10.0.25.173 gateway.nstest.local VIP
Windows Server 2019 10.0.25.174 ad.nstest.local 域控服务器,用于给Citrix服务器发放证书
Windows 11 10.x.x.x 本机Client机器,能够访问Citrix VIP即可

由于SAML服务需要使用域名进行访问,还需要在本机hosts文件中新加入一个DNS解析条目

访问方式

当我们访问https://gateway.nstest.local时,浏览器会自动跳转到Microsoft的认证界面

输入用户名密码后,会重定向到Gateway的管理界面,到这里就算搭建成功了。

测试方式

推荐使用BurpSuite的SAMLRaider: SAML2 Burp Extension插件进行渗透测试[4],可以很方便地编码解码并修改认证请求包和认证响应包,我们可以设置参数过滤只用来捕获SAML认证过程中的SAMLResponse包,这是IDP认证后通过浏览器发给登录服务的认证响应包,包含了关键的身份认证信息。

如下所示,这个插件可以很方便地修改SAML断言信息,还可以进行常用的SAML攻击。(在Citrix环境下,我测试了所有的这些攻击,都能够被Citrix过滤)

定位漏洞程序

在漏洞通告刚发布时,Citrix官网删除了受漏洞影响版本的上一版本的下载链接,给漏洞diff分析造成了一定困难,而近期Citrix官网放出了距受漏洞影响版本的较近版本12.1-64.17,故重新从diff层面对其分析。

下载Citrix-Gateway-KVM-12.1-64.17(受影响)和Citrix-Gateway-KVM-12.1-65.25(修复版本) 对应的虚拟机镜像,运行后尝试通过挂载提取文件,由于文件系统不同,此种方法较为复杂。所幸Gateway支持ssh连接,可以通过ssh提取出相关文件。

根据NSA披露的缓解措施[5]判断很可能是netscaler组件的nsppe文件出问题,如下图所示。同时 根据信息可以推断很可能是缓冲区溢出类型的漏洞。

漏洞分析

绕过看门狗进程pitboss

Citrix Gateway虚拟机中自带gdb工具,虽然版本有点低而且缺少很多命令,但也将就能用。当我尝试用gdb对nsppe进程进行attach时,发现一旦attach该进程,该进程就会自动重启,看来是有反调试。

通过查看dmesg系统日志得知,有一个pitboss进程会接收nsppe进程的心跳包,如果心跳包丢失超过一定阈值,pitboss进程会向nsppe进程发信号终止掉进程然后重启该进程,当gdb对nsppe进程attach时导致nsppe进程被挂起,pitboss进程接收不到心跳包了也就重启nsppe进程,这就导致无法正常调试nsppe进程。

推测系统应该有自带的工具可以更改这些策略。找了一下果然发现netscaler目录下的pb_policy程序可以设置这些策略,忽略进程挂起的命令如下所示:

[email protected]# /netscaler/pb_policy -h nothing
Current pitboss policy is 0x29b4 (10676):
        PB_ABRT_CULPRIT | PB_RESTART_CULPRIT | PB_RESTART_SYSTEM | PB_KILL_USER_PROCS | PB_WAIT_CORES | PB_REBOOT_ON_SLOW_WARMSTART | PB_REBOOT_ON_INCOMPLETE_REG

Hung processes will be sent a SIGABRT (PB_ABRT_CULPRIT).
Monitored processes which exit will be restarted up to 5 times, except for
packet engines (PB_RESTART_CULPRIT).
If pitboss decides not to restart some failing process(es) all non-failing
processes will be sent a SIGKILL (PB_KILL_USER_PROCS).

Pitboss will then wait for all core dumps to complete (PB_WAIT_CORES) and then
do a warm restart (if a packet engine failed) and otherwise reboot the system (PB_RESTART_SYSTEM).

If startup failure is detected do nothing.

If warmstart takes too long pitboss will reboot the system (PB_REBOOT_ON_SLOW_WARMSTART).

On incomplete registration of mandatory processes after warmstart pitboss will
reboot the system (PB_REBOOT_ON_INCOMPLETE_REG).

Log messages from pitboss will take the default path.

New pitboss policy is 0x29b0 (10672):
        PB_RESTART_CULPRIT | PB_RESTART_SYSTEM | PB_KILL_USER_PROCS | PB_WAIT_CORES | PB_REBOOT_ON_SLOW_WARMSTART | PB_REBOOT_ON_INCOMPLETE_REG

Hung processes will be ignored.
Monitored processes which exit will be restarted up to 5 times, except for
packet engines (PB_RESTART_CULPRIT).
If pitboss decides not to restart some failing process(es) all non-failing
processes will be sent a SIGKILL (PB_KILL_USER_PROCS).

Pitboss will then wait for all core dumps to complete (PB_WAIT_CORES) and then
do a warm restart (if a packet engine failed) and otherwise reboot the system (PB_RESTART_SYSTEM).

If startup failure is detected do nothing.

If warmstart takes too long pitboss will reboot the system (PB_REBOOT_ON_SLOW_WARMSTART).

On incomplete registration of mandatory processes after warmstart pitboss will
reboot the system (PB_REBOOT_ON_INCOMPLETE_REG).

Log messages from pitboss will take the default path.

执行命令后就可以愉快的调试nsppe进程了,对freebsd的内核交互机制不太熟悉,因此就没再详细分析这种看门狗机制,后面有时间可以研究下怎么实现的。

diff分析

将Gateway-12.1-64.17和Gateway-12.1-65.25不同版本的nsppe程序导入 IDA 分析,使用 bindiff 插件进行比较,程序较大需要分析较长时间。diff完成后按照相似度排序,可见新版本修改了一些saml相关的函数,此版本还一并修复了更早的一个身份认证绕过漏洞CVE-2022-27510

逐个分析代码差异,重点关注边界条件修改的函数。一通分析后,发现ns_aaa_saml_entity_encode_decode函数比较可疑,这个函数在新版本被改名为ns_aaa_entity_encode_decode,两者控制流图差异如下,很明显的发现新版本多了一条条件判断路径。

具体来说,老版本12.1-64.17该函数简化后的反汇编代码:

__int64 __fastcall ns_aaa_saml_entity_encode_decode(__int64 a1, __int64 a2, int a3, __int64 a4)
{
  __int64 v5; // rax
  __int64 v6; // rbx
  __int64 v7; // rbx
  int v8; // r9d
  int v9; // r9d
  unsigned __int16 v10; // ax
  unsigned int v11; // eax
  unsigned int v12; // r12d
  __int64 v14; // [rsp+18h] [rbp-58h] BYREF
  __int64 v15[2]; // [rsp+20h] [rbp-50h] BYREF
  int v16; // [rsp+30h] [rbp-40h]
  int v17; // [rsp+34h] [rbp-3Ch]
  int v18; // [rsp+38h] [rbp-38h]
  int v19; // [rsp+3Ch] [rbp-34h]
  int v20; // [rsp+40h] [rbp-30h]

  v15[0] = 0LL;
  v15[1] = a1;
  v16 = a3;
  v17 = a3;
  v18 = 4;
  v19 = 22;
  LOBYTE(v20) = v20 & 0xE0;
  v20 = (32 * ASTR_NOT_REF_COUNTED) | v20 & 0x1F;
  v5 = astr_canonicalize(*(_QWORD *)(*((_QWORD *)cur_as_partition + 2) + 8LL), 5LL, v15, a4, 0LL, 0LL);
  v6 = v5;
  if ( v5 )
  {
    ns_bcopy_(*(_QWORD *)(v5 + 8), a2, *(unsigned int *)(v5 + 16));
    v12 = *(_DWORD *)(v6 + 16);
    astr_destroy(*(_QWORD *)(*((_QWORD *)cur_as_partition + 2) + 8LL), 5LL, v6);
  }
  else
  {
    ......// 日志记录
    return 0;
  }
  return v12;
}

新版本12.1-65.25反汇编代码:

__int64 __fastcall ns_aaa_entity_encode_decode(__int64 a1, __int64 a2, int a3, unsigned int a4, unsigned int a5)
{
  __int64 v7; // rax
  __int64 v8; // r12
  __int64 v9; // rbx
  int v10; // r9d
  int v11; // r9d
  unsigned __int16 v12; // ax
  unsigned int v13; // eax
  unsigned int v14; // ebx
  unsigned int v15; // eax
  __int64 v16; // rbx
  int v17; // r8d
  int v18; // r9d
  int v19; // r8d
  int v20; // r9d
  unsigned __int16 v21; // ax
  unsigned int v22; // eax
  char v24; // [rsp+0h] [rbp-80h]
  char v25; // [rsp+0h] [rbp-80h]
  char v26; // [rsp+0h] [rbp-80h]
  char v27; // [rsp+0h] [rbp-80h]
  __int64 v28; // [rsp+18h] [rbp-68h] BYREF
  __int64 v29[2]; // [rsp+20h] [rbp-60h] BYREF
  int v30; // [rsp+30h] [rbp-50h]
  int v31; // [rsp+34h] [rbp-4Ch]
  int v32; // [rsp+38h] [rbp-48h]
  int v33; // [rsp+3Ch] [rbp-44h]
  int v34; // [rsp+40h] [rbp-40h]

  v29[0] = 0LL;
  v29[1] = a1;
  v30 = a3;
  v31 = a3;
  v32 = 4;
  v33 = 22;
  LOBYTE(v34) = v34 & 0xE0;
  v34 = (32 * ASTR_NOT_REF_COUNTED) | v34 & 0x1F;
  v7 = astr_canonicalize(*(_QWORD *)(*((_QWORD *)cur_as_partition + 2) + 8LL), 5LL, v29, a5, 0LL, 0LL);
  v8 = v7;
  if ( v7 )
  {
    v15 = *(_DWORD *)(v7 + 16);
    if ( v15 <= a4 )
    {
      ns_bcopy_(*(_QWORD *)(v8 + 8), a2, v15);
      v14 = *(_DWORD *)(v8 + 16);
      astr_destroy(*(_QWORD *)(*((_QWORD *)cur_as_partition + 2) + 8LL), 5LL, v8);
    }
    else
    {
      ...... //日志记录
      astr_destroy(*(_QWORD *)(*((_QWORD *)cur_as_partition + 2) + 8LL), 5LL, v8);
      return 0;
    }
  }

  else
  {
    ...... //日志记录
    return 0;
  }
  return v14;
}

可以发现,新版本的ns_aaa_entity_encode_decode函数多了一个a4参数,只有v15变量小于传入的a4参数值,才进行后面的ns_bcopy_内存复制函数,将astr_canonicalize函数返回的结构体偏移0x8位置指向的内存复制到参数a2指向的内存中,复制长度是上面astr_canonicalize函数返回的结构体偏移0x10的成员。对比下两个版本的astr_canonicalize函数并无明显差异。因此继续分析上层函数,查看ns_aaa_entity_encode_decode函数的交叉引用,有不少位置调用了该函数。

我们尝试在该ns_aaa_entity_encode_decode函数打个断点,通过访问https://gateway.nstest.local看能不能断下来。

(gdb) b ns_aaa_saml_entity_encode_decode
Breakpoint 1 at 0xbebfb4
(gdb) c
Continuing.
Breakpoint 1, 0x0000000000bebfb4 in ns_aaa_saml_entity_encode_decode ()
(gdb) bt
#0  0x0000000000bebfb4 in ns_aaa_saml_entity_encode_decode ()
#1  0x0000000000bf8356 in ns_aaa_saml_verify_signature ()
#2  0x0000000000c216ca in ns_aaa_saml_process_data ()
#3  0x0000000000c25577 in ns_aaa_process_saml_req ()
#4  0x0000000000c25842 in ns_aaa_saml_auth ()
#5  0x00000000007c6a45 in ns_vpn_process_unauthenticated_request ()
#6  0x000000000080a326 in ns_aaa_cookie_valid ()
#7  0x000000000081ca31 in ns_aaa_client_handler ()
#8  0x0000000001c2e6a1 in nshttp_input ()
#9  0x0000000001c2433b in nshttp_handler ()
#10 0x00000000016e495e in ns_async_restart_http ()
#11 0x0000000000bfaf40 in ns_aaa_saml_canon_resp_handler ()
#12 0x000000000079ab48 in nsaaa_handler ()
#13 0x0000000001c6d149 in nstcp_input ()
#14 0x0000000001c59e0a in handleL4Session ()
#15 0x0000000001c5724f in dispatch_tcp ()
#16 0x00000000010cf0eb in vmpe_intf_loop_rx_proc ()
#17 0x0000000001c55472 in vc_poll ()
#18 0x00000000015b5ae3 in ns_netio ()
#19 0x00000000015baf4b in packet_engine ()
#20 0x00000000019bd9a3 in ns_enter_main ()
#21 0x00000000019c1fe9 in main ()

成功断了下来,通过调用栈可以看出来这里是处理SAML响应的流程,到这里可以基本判定这里是漏洞点了。

我们继续分析新版本上层ns_aaa_saml_verify_signature函数,可以发现传入的第四个参数(即用来长度比较的参数)是0x800,第二个参数v78(即内存复制目的位置的参数)是一个栈变量,栈空间刚好是0x800大小。

而老版本直接传入了栈变量v78,到这里栈溢出已经呼之欲出了。

通过逆向分析得知该函数是对SAMLResponse进行签名验证,我们在调用ns_bcopy_位置处打个断点看看复制的源内存是什么数据。

(gdb) b *0xBEC15A
Breakpoint 1 at 0xbec15a
(gdb) c
Continuing.

Breakpoint 1, 0x0000000000bec15a in ns_aaa_saml_entity_encode_decode ()
(gdb) x/10i $rip
0xbec15a <ns_aaa_saml_entity_encode_decode+426>:        
    callq  0x1c5e390 <ns_bcopy_>
0xbec15f <ns_aaa_saml_entity_encode_decode+431>:        mov    0x10(%rbx),%r12d
0xbec163 <ns_aaa_saml_entity_encode_decode+435>:        
    mov    27378486(%rip),%rax        # 0x26084a0 <cur_as_partition>
0xbec16a <ns_aaa_saml_entity_encode_decode+442>:        mov    0x10(%rax),%rax
0xbec16e <ns_aaa_saml_entity_encode_decode+446>:        mov    0x8(%rax),%rdi
0xbec172 <ns_aaa_saml_entity_encode_decode+450>:        mov    %rbx,%rdx
0xbec175 <ns_aaa_saml_entity_encode_decode+453>:        mov    $0x5,%esi
0xbec17a <ns_aaa_saml_entity_encode_decode+458>:        
    callq  0x1b4b2a0 <astr_destroy>
0xbec17f <ns_aaa_saml_entity_encode_decode+463>:        
    jmp    0xbec187 <ns_aaa_saml_entity_encode_decode+471>
0xbec181 <ns_aaa_saml_entity_encode_decode+465>:        mov    $0x0,%r12d
(gdb) x/10gx $rdi
0x1115fe018:    0x5057344157596753      0x356e674e66623269
0x1115fe028:    0x7155336a5a465335      0x2f5a6c7272483653
0x1115fe038:    0x544247674f624d77      0x337a446e39775850
0x1115fe048:    0x654765743451734b      0x30756d4e5a536b4c
0x1115fe058:    0x3461474947764668      0x5a38303835356d64
(gdb) set print elements 0
(gdb) x/s $rdi
0x1115fe018:     "SgYWA4WPi2bfNgn55SFZj3UqS6HrrlZ/wMbOgGBTPXw9nDz3KsQ4teGeLkSZNmu0hFvGIGa4dm55808Zuikx4s1rIbTiuyw1z5VkZGuXLl31mObPvrbowtqoBgaeTfAwImtJrw4g2kQoe35b/Z0AgSlu9/LxKRKTaG1jYk6chGNJpKTBCmEqRWKFtJsPjnB9xkAiYspO1T2AsgR9KAq9+cV93X/ZtPkfutRj4IaI3LcMnDxQ+9Pb75HYBZ9LYVqOPGowGVf/Opz40VU6xyWzRlg45ouEHTFS45xCPCe/eQe3mPjsp/kMGsM2e6611stx3Isu+GMgwDGd5hlRp4lFdQ=="

复制的源数据是一串字符串,我们前往burp中看一下流量包刚好是<SignatureValue>字段中的数据,因此很自然地想到构造超长字符串替换<SignatureValue>标签的内容。

前面我们看到v78变量距离栈底部0x890字节,因此构造如下内容:'A'*0x890+'B'*8+'C'*8放入<SignatureValue>标签中,然后在ns_aaa_saml_verify_signature函数最后一条ret指令打个断点

成功验证了栈溢出漏洞存在

漏洞利用

查看下nsppe进程的保护机制,没有canary,栈可执行,程序没有aslr无需泄露基址,可控栈空间很大,似乎是很容易利用。

但很快就发现事情似乎没那么简单,Citrix接收到html中的SAMLResponse响应后,将响应base64解码后转换为xml文本,而根据W3C的标准,以下\x00-\x08?\x0b-\x0c?\x0e-\x1f16进制的字符是不被允许出现在XML文件中的,即使放在<![CDATA[]]> 中,也不能幸免。

也就是说,我们只能控制栈变量到返回地址之间的栈空间,且可控的栈内容不能包含以上字符,因此只能放入经过编码的shellcode。而我们的程序高地址都是\x00,也无法在栈中构造ROP链,只有一次覆盖返回地址低位3字节的机会。

可以寻找到合适的gadget将控制流转移到可控栈空间内实现RCE,也可以控制返回地址到大部分任意函数进行恶意操作。

参考文章

  1. 进宫 SAML 2.0 安全

  2. How to Hunt Bugs in SAML; a Methodology - Part I

  3. CitrixADC 四种常见的拓扑模式以及MIP,SNIP的区别

  4. How to Hunt Bugs in SAML; a Methodology - Part II

  5. APT5: Citrix ADC Threat Hunting Guidance


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2049/


文章来源: https://paper.seebug.org/2049/
如有侵权请联系:admin#unsafe.sh