任意 .json 文件读取,无需登录。这是在阅读 agit8or1/OPNMGR 源码时发现的,这是一个用 PHP 编写的 OPNsense 集群管理器。
| CVE | CVE-2026-103978 |
| 安全公告 | GHSA-8x7v-vwpx-3wr7 |
| 类别 | CWE-22(路径遍历) |
| 认证 | 无 |
| CVSS v3.1 | 7.5 高危(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS v4.0 | 8.7 高危 |
| 修复版本 | v3.10.0 |
| 致谢 | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php 位于 Web 根目录下,直接从查询字符串中获取 scan_id,将其拼接到文件路径中,然后把结果交给 file_get_contents()。没有引入认证文件,没有会话检查,也没有对参数进行任何过滤。
<?php
header('Content-Type: application/json');
$scan_id = $_GET['scan_id'] ?? ''; // 未认证,原始输入
if (empty($scan_id)) { echo json_encode(['error' => 'No scan ID provided']); exit; }
$progress_file = "/tmp/snyk_scan_{$scan_id}.json"; // 字符串拼接,仅此而已
if (!file_exists($progress_file)) { echo json_encode(['error' => 'Scan not found']); exit; }
$progress_data = file_get_contents($progress_file);
echo $progress_data; // 返回给调用者
应用中的其他所有端点在做任何事之前都会引入 inc/bootstrap.php 并调用 requireLogin()。而这里跳过了所有这些。因此 scan_id 由攻击者控制,原样落入路径中,文件内容随响应返回。
有一个小问题:前缀 snyk_scan_ 会粘在你发送的任何内容前面,所以以 .. 开头的 payload 会被吞掉(snyk_scan_.. 只是 /tmp 中一个奇怪的文件名)。你需要先放一个一次性片段,这样 .. 才能真正成为向父目录的跳转:
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
末尾的 .json 是代码强制添加的,而且现代 PHP 中不存在空字节技巧,所以你只能读取以 .json 结尾的文件。在真实的 PHP 部署中,这算不上多大的限制。配置文件、服务账号密钥、composer 的 auth.json、云凭证,大多数有价值的东西本来就是 .json。
GET /snyk_scan_progress.php?scan_id=x/../../var/www/opnsense/config
-> 返回 /var/www/opnsense/config.json
GET /snyk_scan_progress.php?scan_id=x/../../../../etc/some_config
-> 遍历到文件系统根目录,返回 /etc/some_config.json
poc/poc.sh 中有一个可运行的版本。
任何以 .json 结尾且 www-data 可读的文件,都会被任何能访问该页面的人泄露。在管理 OPNsense 防火墙的机器上,这通常意味着 API 密钥和凭证。
添加应用其余部分使用的认证引入,并在构建路径之前根据严格的白名单验证 scan_id:
require_once __DIR__ . '/inc/bootstrap.php';
requireLogin();
$scan_id = $_GET['scan_id'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]{1,64}$/', $scan_id)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid scan id']);
exit;
}
$progress_file = '/tmp/snyk_scan_' . $scan_id . '.json';
维护者在 v3.10.0 中发布了修复。
通过协调披露报告。该漏洞版本已不再是当前发布版本。本文仅供参考和学习。