

(CVE‑2022‑27924 / “Pre‑Auth RCE” – exploited in the wild before public disclosure)
⚠️ Disclaimer – The code below is strictly for defensive purposes (version checking, harmless probing, and remediation guidance). It must not be used to attempt unauthorized access, privilege escalation, or any malicious activity. Always obtain explicit permission before scanning any system you do not own or administer.
| Item | Why you need it | Recommended version |
|---|---|---|
| Python 3.9+ | Runs the detection script | python --version |
| Node.js 14+ (or Deno) | Runs the JS/TS detection script | node --version |
| Git | Clone the example repo (optional) | git --version |
| Access to a Zimbra instance (test lab or your own server) | To verify the script works against a known‑good target | – |
| API key / token (if your Zimbra exposes a protected admin API) | Only needed if you want to query authenticated endpoints – never hard‑code credentials in public repos | – |
Basic networking tools (curl, openssl) | For manual verification | – |
Note: The scripts only use public, unauthenticated HTTP endpoints (e.g., the Zimbra login page or the public SOAP
/service/soapendpoint) to retrieve the server version string. No exploitation attempts are made.
git clone https://github.com/icarax/zimbravuln-detector.git
cd zimbravuln-detector
# Create a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies
pip install --upgrade pip
pip install requests tqdm
# Initialise a new npm project (if you don't have one already)
npm init -y
# Install core dependencies
npm install axios yargs chalk
# Install TypeScript and type definitions (if you prefer TS)
npm install --save-dev typescript @types/node @types/yargs ts-node
# Create a basic tsconfig.json
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es6
Below are two complete, copy‑and‑paste ready scripts that:
The version list is deliberately kept short for illustration. In production you should pull the latest CVE data from a trusted feed (e.g., NVD, OSV, or Zimbra’s security advisory page).
detect_zimbra.py)#!/usr/bin/env python3
"""
Zimbra version detector – defensive use only.
Checks a target Zimbra server for the publicly exposed version string
and compares it against a list of known vulnerable releases.
"""
import sys
import argparse
import requests
from typing import List, Tuple
# ----------------------------------------------------------------------
# Known vulnerable versions (as of 2024-09). Add more as advisories appear.
# Format: (major, minor, patch) – inclusive upper bound.
# ----------------------------------------------------------------------
VULNERABLE_RANGES: List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]] = [
# Example: CVE‑2022‑27924 affects 8.8.15 < 8.8.15 P11
((8, 8, 15, 0), (8, 8, 15, 10)), # versions < 8.8.15 P11
# Add further ranges here, e.g. ((8, 8, 15, 11), (8, 8, 15, 15)) …
]
# ----------------------------------------------------------------------
def parse_version_string(raw: str) -> Tuple[int, int, int, int]:
"""
Convert a Zimbra version string like "8.8.15_GA_3869.RHEL7_64" into
a tuple (major, minor, patch, build) for easy comparison.
Non‑numeric parts are ignored.
"""
# Keep only digits and dots, split on non‑digit boundaries
parts = []
num = ''
for ch in raw:
if ch.isdigit() or ch == '.':
num += ch
else:
if num:
parts.append(int(num))
num = ''
if num:
parts.append(int(num))
# Pad to at least 4 components (major, minor, patch, build)
while len(parts) < 4:
parts.append(0)
return tuple(parts[:4]) # ignore anything beyond build
def is_vulnerable(version: Tuple[int, int, int, int]) -> bool:
"""Check if the version falls inside any vulnerable range."""
for low, high in VULNERABLE_RANGES:
if low <= version < high:
return True
return False
def fetch_zimbra_version(base_url: str, timeout: int = 10) -> str:
"""
Attempt to retrieve the Zimbra version from a public endpoint.
The login page (`/`) often contains a comment with the version.
Fallback: try the SOAP endpoint which returns a SOAP Fault that
includes the server version in the faultstring.
"""
headers = {"User-Agent": "ZimbraVersionDetector/1.0 (+https://icarax.example)"}
try:
# 1️⃣ Try the root page – many installations embed version in HTML comment
resp = requests.get(base_url, headers=headers, timeout=timeout, verify=False)
resp.raise_for_status()
html = resp.text
# Look for a pattern like "8.8.15_GA_3869"
import re
m = re.search(r'(\d+\.\d+\.\d+_GA_\d+)', html)
if m:
return m.group(1)
# 2️⃣ Fallback: SOAP endpoint – send a minimal envelope and read fault
soap_url = f"{base_url.rstrip('/')}/service/soap"
soap_payload = """<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<GetVersionRequest xmlns="urn:zimbraAdmin"/>
</soap:Body>
</soap:Envelope>"""
resp = requests.post(
soap_url,
data=soap_payload,
headers={**headers, "Content-Type": "text/xml; charset=utf-8"},
timeout=timeout,
verify=False,
)
resp.raise_for_status()
# The faultstring often contains the version, e.g. "Version: 8.8.15_GA_3869"
m = re.search(r'Version:\s*([\d\.]+_GA_\d+)', resp.text)
if m:
return m.group(1)
except requests.RequestException as e:
raise RuntimeError(f"Failed to query {base_url}: {e}") from e
raise RuntimeError("Could not determine Zimbra version from the target.")
def main() -> None:
parser = argparse.ArgumentParser(
description="Defensive Zimbra version scanner (detects known vulnerable releases)."
)
parser.add_argument(
"-u",
"--url",
required=True,
help="Base URL of the Zimbra server (e.g., https://mail.example.com)",
)
parser.add_argument(
"-t",
"--timeout",
type=int,
default=10,
help="HTTP timeout in seconds (default: 10)",
)
parser.add_argument(
"--no-verify",
action="store_true",
help="Disable TLS certificate verification (use only in trusted labs)",
)
args = parser.parse_args()
if args.no_verify:
# Suppress only the single InsecureRequestWarning from urllib3 needed for demo
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
try:
raw_version = fetch_zimbra_version(args.url, timeout=args.timeout)
version_tuple = parse_version_string(raw_version)
vulnerable = is_vulnerable(version_tuple)
print(f"Target: {args.url}")
print(f"Detected Zimbra version: {raw_version}")
if vulnerable:
print("⚠️ VERSION APPEARS VULNERABLE – apply vendor patches immediately!")
sys.exit(1)
else:
print("✅ Version does NOT match any known vulnerable ranges (as of this script's data).")
sys.exit(0)
except Exception as exc:
print(f"❌ Error: {exc}", file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main()
# Make sure you are inside the venv
source .venv/bin/activate
python detect_zimbra.py -u https://mail.example.com --no-verify
Tip: Remove
--no-verifywhen scanning production hosts with valid TLS certs.
detectZimbra.ts)#!/usr/bin/env node
/**
* Defensive Zimbra version detector – Node.js/TypeScript.
* Mirrors the functionality of the Python script.
*/
import axios from "axios";
import * as yargs from "yargs";
import { hideBin } from "yargs/helpers";
import chalk from "chalk";
// ---------------------------------------------------------------------
// Known vulnerable ranges (same as Python script)
// ---------------------------------------------------------------------
type Version = [number, number, number, number]; // [major, minor, patch, build]
const VULNERABLE_RANGES: [Version, Version][] = [
// Example: < 8.8.15 P11
[[8, 8, 15, 0], [8, 8, 15, 10]],
// Add more ranges as needed
];
// ---------------------------------------------------------------------
function parseVersionString(raw: string): Version {
const nums: number[] = raw.match(/\d+/g)?.map(Number) ?? [];
// Pad to 4 parts
while (nums.length < 4) nums.push(0);
return [nums[0]!, nums[1]!, nums[2]!, nums[3]!];
}
function isVulnerable(v: Version): boolean {
return VULNERABLE_RANGES.some(([low, high]) => {
return (
low[0] <= v[0] &&
low[1] <= v[1] &&
low[2] <= v[2] &&
low[3] <= v[3] &&
v[0] < high[0] ||
(v[0] === high[0] && v[1] < high[1]) ||
(v[0] === high[0] && v[1] === high[1] && v[2] < high[2]) ||
(v[0] === high[0] && v[1] === high[1] && v[2] === high[2] && v[3] < high[3])
);
});
}
// ---------------------------------------------------------------------
async function fetchZimbraVersion(baseUrl: string, timeout = 10000): Promise<string> {
const headers = {
"User-Agent": "ZimbraVersionDetector/1.0 (+https://icarax.example)",
};
// 1️⃣ Try root page
try {
const { data } = await axios.get(baseUrl, { headers, timeout, validateStatus: () => true });
const match = data.match(/(\d+\.\d+\.\d+_GA_\d+)/);
if (match) return match[1];
} catch (_) {
// ignore – fall back to SOAP
}
// 2️⃣ SOAP fallback
const soapUrl = `${baseUrl.replace(/\/+$/, "")}/service/soap`;
const soapPayload = `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<GetVersionRequest xmlns="urn:zimbraAdmin"/>
</soap:Body>
</soap:Envelope>`;
try {
const { data } = await axios.post(soapUrl, soapPayload, {
headers: { ...headers, "Content-Type": "text/xml; charset=utf-8" },
timeout,
validateStatus: () => true,
});
const match = data.match(/Version:\s*([\d\.]+_GA_\d+)/);
if (match) return match[1];
} catch (_) {
// ignore
}
throw new Error("Unable to determine Zimbra version from the target.");
}
// ---------------------------------------------------------------------
function main() {
const argv = yargs(hideBin(process.argv))
.option("url", {
alias: "u",
type: "string",
demandOption: true,
description: "Base URL of the Zimbra server (e.g., https://mail.example.com)",
})
.option("timeout", {
alias: "t",
type: "number",
default: 10000,
description: "HTTP timeout in ms",
})
.option("no-verify", {
type: "boolean",
default: false,
description: "Disable TLS verification (lab use only)",
})
.help()
.argv;
if (argv["no-verify"]) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
}
fetchZimbraVersion(argv.url as string, argv.timeout as number)
.then((raw) => {
const ver = parseVersionString(raw);
const vulnerable = isVulnerable(ver);
console.log(chalk.bold(`Target: ${argv.url}`));
console.log(`Detected Zimbra version: ${raw}`);
if (vulnerable) {
console.log(chalk.red.bold("⚠️ VERSION APPEARS VULNERABLE – apply patches immediately!"));
process.exit(1);
} else {
console.log(chalk.green("✅ Version does NOT match known vulnerable ranges (script data)."));
process.exit(0);
}
})
.catch((err) => {
console.error(chalk.red(`❌ Error: ${err.message}`));
process.exit(2);
});
}
// ---------------------------------------------------------------------
if (require.main === module) {
main();
}
# If you saved as .ts and want to run via ts-node:
npx ts-node detectZimbra.ts -u https://mail.example.com --no-verify
# Or compile to JS first:
npx tsc
node dist/detectZimbra.js -u https://mail.example.com --no-verify
Note: The scripts deliberately avoid sending any malicious payloads. They only perform read‑only HTTP requests that a normal browser or monitoring tool would make.
| Variable | Where to set | Example | Usage |
|---|---|---|---|
ZIMBRA_URL | .env or CLI --url | https://mail.example.com | Target Zimbra base URL |
HTTP_TIMEOUT | .env or CLI --timeout | 15 | Seconds (Python) / ms (Node) |
SKIP_TLS_VERIFY | .env or CLI --no-verify | true | Set to true only in isolated labs |
PROXY (optional) | .env | http://127.0.0.1:8080 | If you need to route through a Burp/ZAP proxy for debugging |
Sample .env (Python)
ZIMBRA_URL=https://mail.example.com
HTTP_TIMEOUT=12
SKIP_TLS_VERIFY=true
Sample .env (Node)
ZIMBRA_URL=https://mail.example.com
HTTP_TIMEOUT=15000
SKIP_TLS_VERIFY=true
You can load the file with dotenv (Python: pip install python-dotenv; Node: npm install dotenv) if you prefer not to pass CLI flags each time.
# Python example – read a list of hosts from a file
hosts = ["https://zimbra1.example.com", "https://zimbra2.example.com"]
for host in hosts:
try:
ver = fetch_zimbra_version(host)
# … same logic as before …
except Exception as e:
print(f"[{host}] ERROR: {e}")
// Node/TS example – async map
const hosts = [
"https://zimbra1.example.com",
"https://zimbra2.example.com",
];
await Promise.all(
hosts.map(async (h) => {
try {
const ver = await fetchZimbraVersion(h);
// … evaluate …
} catch (e) {
console.error(`[${h}] ${e.message}`);
}
})
);
(host, version, timestamp) tuple in a tiny SQLite DB or a JSON file.{"host":"…","version":"…","vulnerable":true/false,"ts":…}vulnerable:true → PagerDuty/Slack alert.Only run this in a fully controlled environment where you have explicit change‑control approval.
# Example: if vulnerable, run the official Zimbra upgrade script via SSH
if vulnerable; then
ssh admin@$HOST "sudo /opt/zimbra/bin/zmcontrol upgrade"
fi
Never hard‑code credentials; use SSH keys or a vault (HashiCorp Vault, AWS Secrets Manager) and audit every execution.
| Symptom | Likely Cause | Fix |
|---|---|---|
SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] | Target uses a self‑signed or expired cert and verification is on. | Add --no-verify (lab) or install the proper CA cert on the scanner host. |
404 Not Found on /service/soap | The Zimbra instance is behind a WAF or proxy that blocks unknown SOAP actions. | Try the root page method (most installations leak version in HTML comment). |
| Script hangs / times out | Network firewall blocking outbound HTTP/HTTPS to the target. | Verify connectivity with curl -v <URL>; adjust firewall or run scanner from a permitted subnet. |
Detected version string looks odd (e.g., 8.8.15_GA_3869.RHEL7_64) but script says “unknown”. | Parsing regex didn’t capture the build suffix. | Extend the regex to allow extra dot‑separated tokens: r'(\d+\.\d+\.\d+_GA_\d+(?:\.[\w\d]+)*)'. |
| False positive: script flags a patched host as vulnerable. | The version list in the script is out‑of‑date. | Pull the latest CVE data from https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories and update VULNERABLE_RANGES. |
| No output, script exits with code 0 but you know the host is old. | The host may have disabled the information leak (custom theme or proxy stripping comments). | Use an authenticated API call (if you have credentials) to request GetVersionRequest via SOAP – this returns the version even when HTML comments are stripped. |
Before deploying the detector in a production monitoring pipeline, verify the following:
| ✅ Item | Why it matters |
|---|---|
| Least‑privilege network access – scanner should only be allowed to initiate outbound HTTPS to the Zimbra hosts (no inbound listeners). | Reduces attack surface if the scanner itself is compromised. |
| TLS verification enabled (except in isolated labs). | Prevents MITM attacks on the scanner’s own traffic. |
| Secrets management – never store Zimbra admin passwords in the script; use a vault or SSH keys if authenticated checks are needed. | Avoids credential leakage. |
Regular updates – schedule a weekly job to refresh the VULNERABLE_RANGES array from the official Zimbra security advisory RSS/JSON feed. | Keeps detection accurate as new patches appear. |
| Idempotent logging – each scan should append a JSON line with timestamp, host, version, and verdict; avoid overwriting previous results. | Enables trend analysis and audit trails. |
| Alert throttling – if many hosts are vulnerable, aggregate alerts (e.g., one summary per 5 min) to avoid alert fatigue. | Keeps SOC teams responsive. |
| Testing in a staging replica – run the scanner against a cloned, non‑production Zimbra instance first. | Confirms the script does not interfere with production service. |
| Documentation & run‑books – keep a short SOP that explains how to interpret the output and what patch to apply. | Ensures consistent response across shifts. |
| Legal & policy compliance – verify that scanning is permitted under your organization’s vulnerability‑management policy and any relevant regulations (e.g., GDPR, PCI‑DSS). | Avoids accidental policy violations. |
VULNERABLE_RANGES as new advisories appear.Stay safe, keep your Zimbra patches up‑to‑date, and remember: defensive scanning is only useful when followed by timely remediation.
ICARAX Tech Blog – Security Automation Series
Author: Your Name – Application Security Engineer
Date: 2025‑09‑26
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
