CVE-2024-53704

Catalpa 网络安全爱好者

2025 年 1 月 8 日,Sonicwall 公开了影响 SonicOS GEN6、GEN7、GEN8 硬件与虚拟化设备的一批漏洞,其中 CVE-2024-53704 被描述为 SSLVPN 身份验证绕过,本文对此漏洞进行简要分析。

没有证据表明该漏洞存在在野利用,但鉴于 PoC 已经公开,建议用户及时更新最新版本。

基本信息

Sonicwall 发布的公告中提到该漏洞为 SSLVPN 服务的身份验证绕过,影响 SonicOS GEN7 7.1.x 以及 GEN8 版本,该公告中没有给出任何具体信息。2025 年 1 月 9 日,ZDI 公开了关于该漏洞更详细的信息,我们可以得知漏洞出现在 SSLVPN 服务验证 session 的过程中,具体来说是 BASE64 格式的 session 验证存在缺陷,导致身份验证绕过。

SSLVPN 鉴权分析

本站的历史文章已经分析过 SonicOS GEN7 7.1.x 版本开始添加的安全启动策略,以及如何解密硬盘获取文件系统,因此这里不再赘述,根据公告,我们分别解包得到 NSv 7.1.2-7019 和 7.1.3-7015 两个版本的 sonicosv 程序,留作后续补丁分析。

由于 7.1.x 版本系统中已经不再留有 sonicosv 的 debug 版本,我们可以先分析 7.0.x 中的程序,看看 SSLVPN 服务是如何对请求进行鉴权的。

在 sonicosv 中,HTTP 请求会首先来到 httpServer 函数处理,它同时负责处理 api、sslvpn 等 web 请求。当我们尝试从网页端登录 SSLVPN 服务时,通过抓包发现发送的请求基本都是 /api/sonicos/xxx,即 API 请求。进一步分析 httpServer 函数,会发现以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ...

else if ( strstr(*(_api_sonicos_ + 7), "/cgi-bin/sshv2?mode=html") )
{
n4 = sub_306E703(
"sshv2.html",
_api_sonicos_[32],
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 10),
a1,
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 13),
*(_api_sonicos_ + 20),
a3);
if ( n4 == -1 )
*(_api_sonicos_ + 7) = 1;
}
else if ( strstr(*(_api_sonicos_ + 7), "/cgi-bin/telnet?html=1") )
{
n4 = sub_306E703(
"telnet.html",
_api_sonicos_[32],
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 10),
a1,
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 13),
*(_api_sonicos_ + 20),
a3);
if ( n4 == -1 )
*(_api_sonicos_ + 7) = 1;
}
else if ( strstr(*(_api_sonicos_ + 7), "/cgi-bin/vnc?mode=html") )
{
n4 = sub_306E703(
"vnc.html",
_api_sonicos_[32],
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 10),
a1,
*(_api_sonicos_ + 9),
*(_api_sonicos_ + 13),
*(_api_sonicos_ + 20),
a3);
if ( n4 == -1 )
*(_api_sonicos_ + 7) = 1;
}
else if ( strstr(*(_api_sonicos_ + 7), "/cgi-bin") )
{
n4 = processSslvpnHttpRequest(a1, _api_sonicos_);
}

// ...

当访问 /cgi-bin 路由时,程序会调用 processSslvpnHttpRequest 函数,从函数名称来看它是用于处理 SSLVPN 请求的。我们知道一般情况下 SSLVPN 服务都会有客户端应用,Sonicwall 也不例外。这个 VPN 客户端叫做 Netextender,用户可以使用客户端连接 vpn 服务器访问内网资源。

可以推测 /cgi-bin 下面的端点可能是客户端应用才会使用的,安装客户端并尝试连接服务,连接成功效果如下

进一步分析 processSslvpnHttpRequest 函数,发现其中注册了很多 /cgi-bin 端点,包括 /cgi-bin/welcome/cgi-bin/homepage 等,除了少数几个不需要身份验证之外,其它端点都需要进行 session 验证。

/cgi-bin/sessionStatus 为例,它对应的处理代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
unsigned __int64 __fastcall sub_2F2EA51(unsigned int a1, __int64 a2, const char *haystack)
{
char *src; // [rsp+1050h] [rbp-80h]
__int64 v6; // [rsp+1058h] [rbp-78h]
_BYTE v7[16]; // [rsp+1070h] [rbp-60h] BYREF
char dest[72]; // [rsp+1080h] [rbp-50h] BYREF
unsigned __int64 v9; // [rsp+10C8h] [rbp-8h]

v9 = __readfsqword(0x28u);
memset(dest, 0, 0x40uLL);
if ( (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(1428, 104LL, "SSLVPNSERVER log from %s:%d, IP: 0x%x", "sslpvnHandleSessionStatus", 3334LL, 0LL, -1LL);
sub_28CD819(1428, 104LL, "====Enter====", 0xFFFFFFFFLL);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "sslpvnHandleSessionStatus", 3334LL);
}
printf("SSLVPNSERVER: (%s %d) ", "sslpvnHandleSessionStatus", 3334);
sub_1F97A1A("====Enter====");
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "sslpvnHandleSessionStatus", 3334LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
src = getSwapCookieFromHttpreq(haystack);
if ( src )
{
strncpy(dest, src, 0x3FuLL);
maybe_url_decode(dest);
v6 = sslvpnVerifyTunnelSession(a1, dest, 1);
if ( v6 )
{
dual_fdprintf(a1, "HTTP/1.0 200 OK\r\n");
dual_fdprintf(a1, "Server: SonicWALL SSLVPN Web Server\r\n\r\n");
dual_fdprintf(a1, "{\"status\":\"found\"}\r\n\r\n");
if ( sub_2ED30E9(a2, "forReconnect", v7, 1LL) && v7[0] == 49 )
*(v6 + 792) |= 2u;
}
else
{
dual_fdprintf(a1, "HTTP/1.0 200 OK\r\n");
dual_fdprintf(a1, "Server: SonicWALL SSLVPN Web Server\r\n\r\n");
dual_fdprintf(a1, "{\"status\":\"notfound\"}\r\n\r\n");
}
if ( (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(
1428,
104LL,
"SSLVPNSERVER log from %s:%d, IP: 0x%x",
"sslpvnHandleSessionStatus",
3358LL,
0LL,
-1LL);
sub_28CD819(1428, 104LL, "====Exit====", 0xFFFFFFFFLL);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "sslpvnHandleSessionStatus", 3358LL);
}
printf("SSLVPNSERVER: (%s %d) ", "sslpvnHandleSessionStatus", 3358);
sub_1F97A1A("====Exit====");
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "sslpvnHandleSessionStatus", 3358LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
}
else if ( (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(1428, 104LL, "SSLVPNSERVER log from %s:%d, IP: 0x%x", "sslpvnHandleSessionStatus", 3337LL, 0LL, -1LL);
sub_28CD819(1428, 104LL, "====Exit====", 0xFFFFFFFFLL);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "sslpvnHandleSessionStatus", 3337LL);
}
printf("SSLVPNSERVER: (%s %d) ", "sslpvnHandleSessionStatus", 3337);
sub_1F97A1A("====Exit====");
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "sslpvnHandleSessionStatus", 3337LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
return __readfsqword(0x28u) ^ v9;
}

从名称来看这个功能应该是用于获取 session 对应会话状态的,除去函数中大量的 LOG 信息,实际上它所执行的操作非常简单,首先使用 getSwapCookieFromHttpreq 函数尝试从请求中获取 Cookie 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const char *__fastcall getSwapCookieFromHttpreq(const char *haystack)
{
int v2; // r9d
char *i; // [rsp+1030h] [rbp-20h]
char *i_1; // [rsp+1038h] [rbp-18h]
const char *v5; // [rsp+1038h] [rbp-18h]

if ( !haystack )
return 0LL;
i_1 = strstr(haystack, "swap=");
if ( !i_1 )
return 0LL;
for ( i = i_1; *i && *i != ';' && *i != 13 && *i != 10; ++i )
;
*i = 0;
v5 = i_1 + 5;
if ( (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(1428, 104, "SSLVPNSERVER log from %s:%d, IP: 0x%x", "getSwapCookieFromHttpreq", 1498, 0);
sub_28CD819(1428, 104, "sslvpnclient cookie:%s", v5, -1, v2);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "getSwapCookieFromHttpreq", 1498LL);
}
printf("SSLVPNSERVER: (%s %d) ", "getSwapCookieFromHttpreq", 1498);
sub_1F97A1A("sslvpnclient cookie:%s", v5);
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "getSwapCookieFromHttpreq", 1498LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
return v5;
}

可以推断 Cookie 中应该包含一个 swap 参数,为 session 值。获取到 session 值以后使用 maybe_url_decode 进行 URL 解码,随后调用 sslvpnVerifyTunnelSession 进行 session 鉴权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
__int64 __fastcall sslvpnVerifyTunnelSession(__int64 a1, const char *session, char a3)
{
__int64 i; // [rsp+1030h] [rbp-20h]

if ( session )
{
sub_2F1F013(session_list);
for ( i = sub_2F1EECB(session_list); i; i = sub_2F1EF2C(i) )
{
if ( (*(i + 792) & 8) == 0 )
{
if ( a3 )
{
if ( !strncmp(session, (i + 465), *(i + 532)) )
break;
}
else if ( !strncmp(session, (i + 432), 0x20uLL) )
{
break;
}
}
}
if ( !i && (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(
1428,
104LL,
"SSLVPNSERVER log from %s:%d, IP: 0x%x",
"sslvpnVerifyTunnelSession",
1477LL,
0LL,
-1LL);
sub_28CD819(1428, 104LL, "%s verify sslvpn session failed!", "sslvpnVerifyTunnelSession", 0xFFFFFFFFLL);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "sslvpnVerifyTunnelSession", 1477LL);
}
printf("SSLVPNSERVER: (%s %d) ", "sslvpnVerifyTunnelSession", 1477);
sub_1F97A1A("%s verify sslvpn session failed!", "sslvpnVerifyTunnelSession");
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "sslvpnVerifyTunnelSession", 1477LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
sub_2F1F175(session_list);
return i;
}
else
{
if ( (dword_A039974 & 2) != 0 )
{
if ( (dword_A039974 & 0x8000) != 0 )
{
sub_28CD819(
1428,
104LL,
"SSLVPNSERVER log from %s:%d, IP: 0x%x",
"sslvpnVerifyTunnelSession",
1444LL,
0LL,
-1LL);
sub_28CD819(1428, 104LL, "NULL cookie!", 0xFFFFFFFFLL);
}
else
{
if ( byte_8D10800 )
{
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D550EB(&mutex_);
sub_27C9E92(&unk_C5C5800, "sslvpnVerifyTunnelSession", 1444LL);
}
printf("SSLVPNSERVER: (%s %d) ", "sslvpnVerifyTunnelSession", 1444);
sub_1F97A1A("NULL cookie!");
if ( byte_8D10800 )
{
sub_27CA075(&unk_C5C5800, "sslvpnVerifyTunnelSession", 1444LL);
if ( __readfsbyte(0xFFFEC2A1) != 1 )
sub_2D55155(&mutex_);
}
}
}
return 0LL;
}
}

这个函数的实现也相对简单,它会遍历保存在内存中的 session_list,尝试和用户提交的 session 进行比较,如果比较成功说明找到了对应会话,表示鉴权成功,反之失败。

7.0.x 实现的是一个相对常规的验证逻辑,我们再来看看 7.1.x 版本此处逻辑是否有修改。

分析 7.1.2-7019 版本程序,sessionStatus 处理函数代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
__int64 __fastcall sessionStatus(unsigned int n2147483646, _DWORD *a2, double a3)
{
__int64 param; // rax
const char *str; // rax
__int64 SslvpnSessionFromCookie; // rbx
char v6; // al

param = get_param(a2, "cookie");
str = get_str(param);
SslvpnSessionFromCookie = getSslvpnSessionFromCookie(str);
v6 = check_session(SslvpnSessionFromCookie);
if ( SslvpnSessionFromCookie && v6 )
{
*(SslvpnSessionFromCookie + 456) = sub_2011070();
sub_26619D0(n2147483646, "HTTP/1.0 200 OK\r\n", a3);
sub_26619D0(n2147483646, "Server: SonicWALL SSLVPN Web Server\r\n\r\n");
sub_26619D0(n2147483646, "{\"status\":\"active\"}\r\n\r\n");
return 0LL;
}
else
{
sub_26619D0(n2147483646, "HTTP/1.0 200 OK\r\n", a3);
sub_26619D0(n2147483646, "Server: SonicWALL SSLVPN Web Server\r\n\r\n");
sub_26619D0(n2147483646, "{\"status\":\"notfound\"}\r\n\r\n");
return 0LL;
}
}

新版本逻辑存在一些变动,例如 get_param 获取参数的方式、移除了很多 LOG 信息等。但最关键的是 session 验证逻辑发生了变化。

getSslvpnSessionFromCookie 函数内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__int64 __fastcall getSslvpnSessionFromCookie(const char *raw_str)
{
size_t raw_str_len; // rax
_BYTE *s_1; // rax
void *s_2; // rbp
__int64 cookie; // [rsp+0h] [rbp-10h]

if ( !raw_str )
return 0LL;
raw_str_len = strlen(raw_str);
if ( raw_str_len == 32 ) // not base64 encoded
{
if ( !verifyCookieCheckSum(raw_str, 32) )
return 0LL;
return find_cookie(raw_str, 1u);
}
else
{
if ( raw_str_len != 44 )
return 0LL;
s_1 = base64_decode(raw_str); // base64 encoded
s_2 = s_1;
if ( !s_1 )
return 0LL;
if ( !verifyCookieCheckSum(s_1, 32) )
{
maybe_free(s_2);
return 0LL;
}
cookie = find_cookie(s_2, 1u);
maybe_free(s_2);
return cookie;
}
}

现在 session 有两种解析方式,当长度为 32 时说明是未编码的数据,当长度为 44 时说明是 BASE64 编码的数据。数据解码之后都会调用 verifyCookieCheckSum 先来检查数据格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__int64 __fastcall verifyCookieCheckSum(_BYTE *raw_str, int size)
{
_BYTE *s_1; // rbx
char v3; // al
unsigned int v4; // r12d
_BYTE *s; // rdi
char buf[26]; // [rsp+Eh] [rbp-1Ah] BYREF

*buf = 0;
if ( size <= 1 )
{
s_1 = raw_str;
}
else
{
s_1 = &raw_str[size - 2 + 1];
v3 = 0;
do
v3 ^= *raw_str++;
while ( raw_str != s_1 );
buf[0] = v3;
}
v4 = 0;
s = base64_encode(buf);
if ( s )
{
LOBYTE(v4) = *s == *s_1;
maybe_free(s);
}
return v4;
}

传入的 32 字节 session 数据最后一个字节表示 checksum,计算方式为从第一个字节到第 31 个字节两两异或,最终得到的一个字节数据使用 BASE64 编码,编码后的结果第一个字节就是 checksum,放在 session 的第 32 个字节位置,接着会调用 find_cookie 函数开始校验 session。

漏洞分析

find_cookie 函数内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__int64 __fastcall find_cookie(__int64 raw_str, unsigned int a2)
{
__int64 obj; // r12
__int64 v3; // rax
char chr; // dl
char target; // cl
int v7; // [rsp+8h] [rbp-20h]

if ( !raw_str )
return 0LL;
obj = *session_list[7 * a2];
if ( !obj )
return obj;
v7 = obj + 40;
lock_list(obj + 40, "ListProtect", 74); // 锁定列表
for ( obj = *(obj + 16); obj; obj = *(obj + 8) )
{
v3 = 0LL;
while ( 1 )
{
chr = *(raw_str + v3); // 获取一个字节
if ( !chr || (target = *(obj + v3 + 28)) == 0 )// 如果 cookie 第一个字节是 null 的话,这里会直接返回第一条记录
{
LABEL_9:
release_list(v7, "ListRelease", 79);
return obj; // <---- here
}
if ( chr != target ) // 比较 session 字节
break;
if ( ++v3 == 32 )
goto LABEL_9;
}
}
release_list(v7, "ListRelease", 79);
return 0LL;
}

我在代码中添加了一些注释,下面我们来详细解释一下查找 session 的流程。

session_list 是一个全局列表,用于保存已经登录的 VPN 用户会话信息,代码先使用 lock_list 锁定 session 列表,然后进入一个循环,先获取列表中的第一个成员,存放到 obj 变量。随后开始逐字节遍历从外部传入的 session 数据,和 obj 中保存的 session 比较,如果 32 个字节都比较成功,则说明找到了正确的会话信息,鉴权成功。

这里的问题在于,代码获取了外部传入 session 的一个字节之后,会先在 if 条件中判断获取到的这个字节是否为 NULL,如果是就释放 session 列表并返回 obj 对象。假设我们传入 session 为 \x00abcd,那么 find_cookie 函数就会直接返回 session 列表中的第一个对象,跳过后续的字符验证,这样就可以直接获得列表中的第一个用户会话信息,造成身份验证绕过。此外,由于 session 由小写字母组成,我们可以控制第二个字节为 NULL,遍历第一个字节来获取 session 列表中的其它用户信息。

参考链接

https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2025-0003

https://www.zerodayinitiative.com/advisories/ZDI-25-012/

https://attackerkb.com/topics/UB3P3xHVAo/cve-2024-53704/rapid7-analysis

  • Title: CVE-2024-53704
  • Author: Catalpa
  • Created at : 2025-01-28 00:00:00
  • Updated at : 2025-01-29 08:46:43
  • Link: https://wzt.ac.cn/2025/01/28/cve-2024-53704/
  • License: This work is licensed under CC BY-SA 4.0.