
Proof-of-concept e writeup per CVE-2026-103978, un path traversal non autenticato in snyk_scan_progress.php di OPNMGR che consente la lettura arbitraria di file .json.
Lettura arbitraria di file .json, senza necessità di login. Trovato durante la lettura del
sorgente di agit8or1/OPNMGR, un gestore di flotte
OPNsense scritto in PHP.
| CVE | CVE-2026-103978 |
| Advisory | GHSA-8x7v-vwpx-3wr7 |
| Classe | CWE-22 (Path Traversal) |
| Auth | nessuna |
| 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 |
| Corretto in | v3.10.0 |
| Credito | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php si trova nella web root e prende un scan_id direttamente dalla
query string, lo incolla in un percorso di file e passa il risultato a
file_get_contents(). Non c'è alcun include di autenticazione, nessun controllo di sessione, nessuna sanificazione
del parametro.
<?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
Ogni altro endpoint dell'applicazione include inc/bootstrap.php e chiama
requireLogin() prima di fare qualsiasi cosa. Questo lo salta del tutto. Quindi scan_id
è controllato dall'attaccante, finisce nel percorso verbatim, e il contenuto del file viene
restituito nella risposta.
Una complicazione: il prefisso snyk_scan_ viene incollato a qualunque cosa si invii, quindi un payload
che inizia con .. viene mangiato (snyk_scan_.. è solo un nome di file strano in /tmp).
Serve prima un segmento usa e getta affinché il .. diventi effettivamente un salto alla directory
superiore:
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
Il .json finale è forzato dal codice e non esiste alcun trucco del null-byte nel
PHP moderno, quindi si possono leggere solo file che terminano in .json. In un deployment PHP reale
non è un gran limite. File di configurazione, chiavi degli account di servizio, auth.json di composer,
credenziali cloud, la maggior parte della roba succosa è già .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
C'è una versione eseguibile in poc/poc.sh.
Qualsiasi file che termina in .json che www-data può leggere, divulgato a chiunque possa
raggiungere la pagina. Su una macchina che gestisce firewall OPNsense questo di solito significa chiavi API
e credenziali.
Aggiungere l'include di autenticazione che usa il resto dell'applicazione, e validare scan_id contro una
allow-list rigida prima di costruire il percorso:
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';
Il maintainer ha rilasciato la correzione nella v3.10.0.
Segnalato tramite divulgazione coordinata. La versione vulnerabile non è più la release corrente. Il writeup è qui per riferimento e apprendimento.