CVE-2023-27997 / XORtigate:FortiGate SSL-VPN 堆溢出复现

Kylinxin Kylinxin

CVE-2023-27997 位于 FortiOS / FortiProxy 的 SSL-VPN 预认证请求处理链。复现重点是从 FortiGate 固件中定位相关二进制,恢复 enc 参数的解码过程,再通过修复前后的二进制差异还原长度处理错误。

以下内容面向隔离的授权研究环境,不包含完整 RCE 利用链。

1. 漏洞与版本选择

漏洞类型为 heap-based buffer overflow,入口位于 SSL-VPN 暴露面。FortiOS 7.2 分支中,7.2.0 至 7.2.4 受影响,7.2.5 修复。

使用 7.2.4 与 7.2.5 组成对照组,可以同时观察旧逻辑、补丁差异和请求行为。

研究对象 版本
FortiOS 7.2 7.2.4 / 7.2.5
FortiOS 7.0 7.0.11 / 7.0.12
FortiOS 6.4 6.4.12 / 6.4.13
FortiOS 6.2 6.2.13 / 6.2.14

复现环境采用 FortiGate VM、仅主机或私有 NAT 网络以及独立测试机。测试机负责抓包和发送受控请求,FortiGate VM 负责记录 SSL-VPN 行为、进程状态和崩溃信息。

两个版本应使用相同网络与 SSL-VPN 配置,避免把配置差异误判为补丁效果。

2. 固件与 rootfs

FortiGate 更新镜像中的关键对象如下:

对象 用途
flatkc kernel image,包含 rootfs 签名检查与解密逻辑
rootfs.gz 用户态根文件系统
*.chk 镜像签名与完整性数据
/bin/init 集成大量用户态逻辑的核心二进制
sslvpnd SSL-VPN 请求处理与运行状态对应的进程

`flatkc` 中的 `fgt_verify_*` 与 `fgt_verifier_*` 符号
`flatkc` 中的 `fgt_verify_*` 与 `fgt_verifier_*` 符号

固件准备不是“解密 kernel”,而是把 flatkc 转成可分析的 ELF,沿 fgt_verify_decryptfgt_verifier_key_iv 恢复 rootfs.gz 的解密参数。

相关调用链会进入 crypto_chacha20_initchacha20_docrypt

`fgt_verify_decrypt` 中的 ChaCha20 调用链
`fgt_verify_decrypt` 中的 ChaCha20 调用链

rootfs 加密方式随 FortiOS 分支变化。7.0.x、7.2.x、7.4.7 和 8.0 的 key 保存位置、符号保留情况和算法并不相同,因此必须以目标版本的 flatkc 为准,不能直接套用其他分支的解密脚本。

FortiGate 7.4.7 rootfs crypto 流程
FortiGate 7.4.7 rootfs crypto 流程

解开 rootfs 后,优先提取 /bin/init 及 SSL-VPN 相关文件,不必遍历整个文件系统。先用 filestrings 和符号信息确定架构、链接方式与目标字符串,再导入 Ghidra 或 IDA。

3. SSL-VPN 请求入口

公开分析涉及三个重要路径:

路径 在分析中的作用
/remote/info 返回请求解密所需的运行时 salt
/remote/hostcheck_validate 处理带 enc 参数的预认证请求
/remote/logincheck 补丁对比与取证研究中出现的相关入口

32 位环境中的请求顺序
32 位环境中的请求顺序

分析顺序应从路径字符串的交叉引用开始,继续追踪参数表、hex decode、MD5 调用和 XOR 循环。这样可以把网络请求中的 enc 参数与二进制内部的长度字段和目标缓冲区对应起来。

4. enc 参数结构

enc 是十六进制编码的数据,由 seed、加密后的 size 和 ciphertext 组成。服务端返回的 salt 与客户端提供的 seed 共同决定 keystream。

`enc` 参数的 seed、size 与 ciphertext
`enc` 参数的 seed、size 与 ciphertext

字段 结构与作用
seed 8 个十六进制字符,参与初始摘要计算
size 2 字节,经 XOR 解密后控制后续处理长度
ciphertext hex decode 后与连续 keystream 做 XOR
salt /remote/info 返回的运行时随机值

处理链可以概括为:

1
2
3
4
5
6
enc hex string
-> hex decode
-> 恢复 size
-> 扩展 MD5 keystream
-> XOR 解密 data
-> 按 size 处理明文

5. MD5/XOR keystream

初始状态由 salt、seed 和固定字符串共同计算。后续摘要以递归方式扩展为足够长的字节流:

1
2
3
4
5
6
S0 = MD5(salt || seed || "GCC is the GNU Compiler Collection.")
S1 = MD5(S0)
S2 = MD5(S1)
...
stream = S1 || S2 || ... || Sn
plaintext = ciphertext XOR stream

salt、seed 与 MD5 keystream
salt、seed 与 MD5 keystream

逆向时要分别标记 size 解密和 data 解密使用的流偏移。只恢复明文而忽略 size 的类型和扩展方式,会错过真正的漏洞点。

1
2
3
decoded = hex_decode(enc);
size = xor_size(decoded.size, stream);
plaintext = xor_data(decoded.data, stream_after_size, size);

6. 长度错误与堆越界写

修复前的核心问题是检查长度与实际处理长度不一致:

  1. enc 经 hex decode 后得到真实字节序列。
  2. 两字节 size 经 XOR 恢复并进入长度计算。
  3. 旧逻辑在 8 位和 16 位值之间使用了不一致的截断或扩展。
  4. 前置边界检查使用较小的值,后续 XOR 循环却按较大的真实长度写入。
  5. 写入跨过已分配堆缓冲区,形成 heap overflow。

keystream XOR 对内存内容的影响
keystream XOR 对内存内容的影响

watchTowr 对修复前后版本的反汇编差异显示,补丁修改了长度变量的读取与扩展方式,其中可见 movzx 相关变化。

对比时应沿变化函数向上找到 endpoint 与参数解析入口,再向下找到分配长度和 XOR 循环,避免只停留在单条指令层面。

修复前后长度处理差异
修复前后长度处理差异

该缺陷并不等同于稳定 RCE。进一步利用还依赖 FortiOS 版本、CPU 架构、堆布局、目标对象与异常恢复行为;复现笔记到边界写与进程行为即可闭环。

7. 复现顺序

7.1 建立对照环境

分别部署 7.2.4 和 7.2.5,记录 build、镜像哈希、SSL-VPN 监听端口和网络拓扑。两台 VM 使用一致配置,只允许测试机访问。

7.2 准备静态分析对象

从固件中取出 flatkcrootfs.gz,根据目标版本恢复 rootfs,提取 /bin/init 和 SSL-VPN 相关文件。记录架构、文件哈希与加载基址,保证反编译结果可重复定位。

7.3 定位参数处理链

在旧版本中搜索以下字符串并建立交叉引用:

1
2
3
4
5
/remote/info
/remote/hostcheck_validate
/remote/logincheck
enc
GCC is the GNU Compiler Collection.

从固定字符串可直接靠近 MD5 初始状态计算;从 enc 和 endpoint 可靠近参数分派。两条路径最终应汇合到 hex decode、size 恢复与 XOR 处理函数。

7.4 对比修复版本

使用 BinDiff、Diaphora 或手工函数匹配对比 7.2.4 与 7.2.5。重点记录:

  • size 的读取宽度和有符号性;
  • 边界检查使用的变量;
  • 缓冲区分配长度;
  • XOR 循环的终止条件;
  • 修复后新增或调整的类型扩展。

7.5 观察请求与进程行为

先用 timing check 在隔离环境中比较两个版本。工具通过不同长度请求的响应时间差判断旧逻辑是否存在,并尽量避免让 sslvpnd 崩溃。

随后用抓包将 /remote/info、后续 endpoint、enc 长度和响应时间与静态分析结果对应。

若进行受控的崩溃实验,应同步记录 sslvpnd 的退出、重启、日志时间和请求时间。崩溃点可能晚于实际堆破坏位置,因此进程退出不能代替对长度链的分析。

请求中的 `enc` 参数痕迹
请求中的 `enc` 参数痕迹

8. timing checker 脚本

8.1 安装与运行

脚本依赖 requestsnumpyscipy。独立虚拟环境可以避免统计库与系统 Python 冲突:

1
2
3
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt

运行时传入 SSL-VPN 接口地址和端口:

1
python3 CVE-2023-27997-check.py 192.168.56.20 10443

脚本会关闭 HTTPS 证书告警,并访问:

1
2
https://192.168.56.20:10443/remote/info
https://192.168.56.20:10443/remote/hostcheck_validate

/remote/info 的响应必须包含形如 salt='89abcdef' 的 8 位十六进制值。没有匹配到唯一 salt 时直接返回 ERROR: not FortiGate ssl vpn?

8.2 enc 头生成

脚本固定使用 seed 00bfbfbf,按漏洞中的摘要关系计算第一组 keystream:

1
2
3
4
magic = b"GCC is the GNU Compiler Collection."
keystream = md5(salt + b"00bfbfbf" + magic).digest()
length = struct.pack("<H", requested_length)
encoded_length = length XOR keystream[:2]

最终请求体由表单参数和十六进制数据组成:

1
2
3
4
ajax=1&username=test&realm=&enc=
00bfbfbf
<2-byte encoded length>
41 41 41 ...

其中 alloc_size 固定为 0xF800。每轮发送两类请求:

分组 encoded length 对应值 data 长度
regular alloc_size // 2 0xF800 字节
overflow alloc_size + 0xF0 0xF800 字节

两组拥有相同的 HTTP body 大小,区别只在解密后的 size。修复版本会更早拒绝不合法长度,旧版本则继续进入后续处理,因此两组响应时间会呈现不同分布。

8.3 采样与统计判定

REQUESTS_PER_GROUP = 400,最坏情况下会发送 400 组 regular 和 400 组 overflow 请求。每轮记录 requests 返回的 elapsed microseconds。

统计处理分为三步:

  1. 分别计算两组样本的 75% 分位数,去掉最高四分之一的延迟尖峰;
  2. 对 overflow 与 regular 样本执行 Welch 独立样本 t-test;
  3. 样本数超过 20 后,每 10 轮重算一次,若 p < 0.001 则提前结束。

最终分类由 t-statistic 决定:

条件 输出
t < -0.5 Patched
t > 0.5 Vulnerable
其他 Unknown

p > 0.001,或 t-statistic 位于 -22 之间时,脚本额外输出 WARNING: Low confidence results.。这里的 Vulnerable 是 timing 分布符合旧版本特征,不表示发生了进程崩溃或代码执行。

8.4 输出与误差处理

典型输出:

1
2
Checking https://192.168.56.20:10443
Vulnerable
1
2
3
Checking https://192.168.56.21:10443
WARNING: Low confidence results.
Unknown

结果不稳定时,按以下顺序排查:

  • 确认端口是 SSL-VPN 接口而不是管理界面;
  • 在同一隔离网段运行,避免公网抖动改变微秒级差异;
  • 暂停会竞争 CPU 或网络的任务;
  • 对同一目标重复运行,比较 t-statistic 的方向是否一致;
  • 在已知旧版本和修复版本上各跑一组,先建立本地基线;
  • 结合 /bin/init 补丁对比,不把单次 timing 输出当作最终结论。

脚本没有请求超时、异常重试和原始采样导出。目标迟迟不响应时可能长时间阻塞;需要长期实验时,应在本地副本中增加 timeout,并把 regular/overflow 原始时间保存为 CSV 后再分析。

9. 分析结论

XORtigate 的完整技术链是:SSL-VPN 预认证入口获取运行时 salt,enc 中的 seed 参与 MD5 迭代,size 与 ciphertext 通过 XOR 恢复,旧版本对长度值的截断和扩展不一致,最终使数据处理循环越过堆缓冲区。

固件解密、字符串交叉引用和补丁对比共同把网络输入与越界写连接起来。

延伸阅读

  • 标题: CVE-2023-27997 / XORtigate:FortiGate SSL-VPN 堆溢出复现
  • 作者: Kylinxin
  • 创建于 : 2025-06-23 09:00:00
  • 更新于 : 2025-06-23 09:00:00
  • 链接: https://kylinxin.github.io/2025/06/23/CVE-2023-27997-XORtigate-FortiGate-SSL-VPN-堆溢出复现/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。