
Proof-of-Concept und Writeup für CVE-2026-103978, einen unauthentifizierten Path Traversal in OPNMGRs snyk_scan_progress.php, der das Lesen beliebiger .json-Dateien ermöglicht.
Beliebiges Lesen von .json-Dateien, kein Login erforderlich. Gefunden beim Durchlesen des
Quellcodes von agit8or1/OPNMGR, einem in PHP
geschriebenen OPNsense-Flottenmanager.
| CVE | CVE-2026-103978 |
| Advisory | GHSA-8x7v-vwpx-3wr7 |
| Klasse | CWE-22 (Path Traversal) |
| Auth | keine |
| 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 |
| Behoben in | v3.10.0 |
| Danksagung | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php liegt im Web-Root, nimmt eine scan_id direkt aus
dem Query-String, klebt sie in einen Dateipfad und übergibt das Ergebnis an
file_get_contents(). Es gibt keinen Auth-Include, keine Session-Prüfung, keine
Bereinigung des Parameters.
<?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
Jeder andere Endpunkt in der Anwendung bindet inc/bootstrap.php ein und ruft
requireLogin() auf, bevor irgendetwas getan wird. Dieser hier überspringt all das. Somit
wird scan_id vom Angreifer kontrolliert, landet unverändert im Pfad, und der Dateiinhalt
kommt in der Antwort zurück.
Ein Haken: Das Präfix snyk_scan_ wird an das angehängt, was man sendet, sodass eine Payload,
die mit .. beginnt, verschluckt wird (snyk_scan_.. ist einfach ein seltsamer Dateiname in /tmp).
Man braucht zuerst ein Wegwerf-Segment, damit die .. tatsächlich zu einem Sprung ins
übergeordnete Verzeichnis wird:
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
Das abschließende .json wird vom Code erzwungen, und es gibt keinen Null-Byte-Trick in
modernem PHP, sodass man nur Dateien lesen kann, die auf .json enden. In einer echten PHP-Bereitstellung
ist das keine große Einschränkung. Konfigurationsdateien, Service-Account-Schlüssel, Composer
auth.json, Cloud-Anmeldedaten – das meiste von dem interessanten Zeug ist bereits .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
Eine ausführbare Version gibt es in poc/poc.sh.
Jede Datei, die auf .json endet und von www-data gelesen werden kann, wird an jeden offengelegt, der
die Seite erreichen kann. Auf einer Maschine, die OPNsense-Firewalls verwaltet, bedeutet das normalerweise
API-Schlüssel und Anmeldedaten.
Den Auth-Include hinzufügen, den der Rest der Anwendung verwendet, und scan_id gegen eine
enge Allow-List validieren, bevor der Pfad gebaut wird:
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';
Der Maintainer hat den Fix in v3.10.0 ausgeliefert.
Gemeldet über koordinierte Offenlegung. Die verwundbare Version ist nicht mehr das aktuelle Release. Der Writeup ist hier als Referenz und zum Lernen.