Debug TryHackMe Walkthrough — PHP Deserialization to Root
Debug TryHackMe Walkthrough — PHP Deserialization to Root
Difficulty: Medium · OS: Linux · Skills: Web enumeration, PHP object injection, hash cracking, Linux privilege escalation
Debug is a medium-rated Linux machine on TryHackMe built around one core idea: insecure PHP deserialization. We start with a tiny attack surface (just SSH and a web server), find a leaked source-code backup, abuse an unserialize() call to get a webshell, then chain a cracked password and a writable MOTD script all the way to root.
This writeup keeps it short and practical so you can follow the logic, not just copy commands. Flags and passwords are intentionally redacted — go earn your own.
Attack Chain at a Glance
- Recon → Only ports 22 (SSH) and 80 (HTTP) are open.
- Web enumeration → A
/backupdirectory leaksindex.php.bak(the site's source code). - Initial access → The source passes user input straight into
unserialize(). We craft a PHP object that abuses the__destruct()magic method to drop a webshell on the liveindex.php→ RCE aswww-data. - Lateral movement → A
.htpasswdhash is cracked; the password is reused by userjames. - Privilege escalation →
jamescan edit MOTD scripts in/etc/update-motd.d/, which run as root on SSH login → root shell.
1. Enumeration
Port Scan
Start with a full Nmap scan to see what's exposed.
Prefer speed? RustScan wraps Nmap and is much faster for the initial sweep:
Result — a very small footprint:
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.10
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
What this tells us: SSH alone is rarely the way in, so the web server on port 80 is almost certainly our entry point.
A Note on SSH (a deliberate rabbit hole)
OpenSSH 7.2p2 is affected by a well-known username enumeration issue (CVE-2018-15473), and you may see tools report users like root. It's worth knowing this exists, but on Debug it's a dead end — the passwords aren't guessable and SSH isn't the intended foothold. Note it, then move on. Recognising rabbit holes quickly is half the skill.
Web Directory Brute-Force
The homepage is just the default Apache page, so we hunt for hidden content.
Interesting hits:
/index.php (the LIVE page — remember this)
/readme.md
/backup (directory listing enabled)
readme.md reveals the site uses the "Base" front-end framework — useful context, but it's only styling, not the vulnerability. The real prize is /backup.
Tip: Directories named backup, dev, old, or bak are gold mines — they often hold source code or credentials never meant to be public.
2. Finding the Vulnerability
Browsing to /backup shows directory listing is enabled, with several .bak files. The HTML backup is just the default Apache page, but index.php.bak is the backup of the live page's source. Download it:
The critical logic:
Why this is exploitable, in plain English
- The
debugparameter is taken from the URL and passed directly intounserialize()with no validation — a textbook insecure deserialization bug. unserialize()rebuilds whatever PHP object we describe, including its property values.- When that object is destroyed at the end of the script, PHP automatically runs the
__destruct()magic method. __destruct()writes to a file whose name (form_file) and contents (message) we fully control.
So we can instruct the server: create a file called shell.php containing PHP of our choosing. That's remote code execution.
Important: the .bak file can't run — target the live index.php
A common beginner trap: you can't exploit index.php.bak, because the server serves it as plain text (it doesn't execute .bak as PHP). The .bak is source disclosure — it just tells us how the code works. The vulnerable unserialize() actually runs on the live /index.php we found during enumeration. That's where we send the payload.
3. Initial Access — Crafting the Payload
Write a small PHP script locally to build the malicious serialized object. Use the same class and property names as the source, set form_file to a .php filename, and set message to a one-line command webshell.
Generate the payload:
Anatomy of the serialized string
Understanding the format helps you debug payloads by hand:
| Token | Meaning |
|---|---|
O:10:"FormSubmit" | an Object whose class name is 10 characters long |
:2: | the object has 2 properties |
s:9:"form_file" | a string property, 9 characters long, named form_file |
s:9:"shell.php" | its value: a 9-character string |
If you ever change the filename or payload, the length numbers must match exactly, or unserialize() rejects it.
Deliver the payload
URL-encode the serialized output and send it through the debug parameter on the live page:
The destructor fires and writes the webshell. Confirm code execution:
We have RCE as www-data.
Upgrade to a Reverse Shell
Start a listener locally:
Trigger a reverse shell through the webshell (URL-encode the command). Because the box has no internet access, host any payload script locally and pull it in. Then stabilise the shell:
4. Lateral Movement — Cracking and Reusing a Password
In the web root there's a hidden Apache credentials file:
The $apr1$ prefix means Apache MD5 (APR1 / md5crypt). Crack it with either tool:
It cracks almost instantly to a common rockyou password. Now check /etc/passwd for real login users, then test whether james reused that password for his system account — a very common real-world mistake:
Password reuse pays off. Grab the user flag:
5. Privilege Escalation — Writable MOTD Scripts
Manual enumeration first
Before reaching for LinPEAS, do the quick manual checks — they often reveal the path instantly:
The .bash_history is the giveaway: it shows previous activity in /etc/update-motd.d/ and an SSH session. That's our breadcrumb.
The note
"root" asks james to make the SSH welcome message prettier and notes he was given write access to "all these files." That welcome banner is the MOTD (Message Of The Day).
The vulnerability
On Ubuntu, scripts in /etc/update-motd.d/ execute as root every time someone logs in over SSH. Check the permissions:
james owns the group and can write to a script that root runs on login. Append a couple of lines to 00-header that copy bash and set the SUID bit (copying to /tmp is cleaner than modifying the system /bin/bash):
Trigger it
Log in over SSH as james — this fires the MOTD scripts as root:
Now drop into a root shell using the SUID binary with the privileged flag:
Rooted. ✅
Key Takeaways
- Never pass untrusted input to
unserialize(). If you must deserialize, use a safe format like JSON, or restrict classes withunserialize($data, ['allowed_classes' => false]). - Source-code backups are a real leak. A stray
.bakexposed the whole vulnerability. Keep backups out of the web root and disable directory listing. .bakfiles don't execute — they disclose. Find the live endpoint to actually trigger the bug.- Don't reuse passwords across services. One cracked Apache hash gave full SSH access.
- MOTD scripts run as root. Any user who can write to
/etc/update-motd.d/effectively owns the box. Audit file ownership and group permissions carefully.
FAQ
What vulnerability does the Debug machine teach?
Insecure PHP deserialization — passing attacker-controlled input to unserialize(), abused via the __destruct() magic method to write a webshell and gain RCE.
Why can't I exploit index.php.bak directly?
The server returns .bak files as plain text, so the PHP never executes. The backup is only for reading the source; you exploit the live index.php.
Which Hashcat mode cracks the .htpasswd hash?
The $apr1$ (Apache MD5 / md5crypt) hash uses Hashcat mode 1600. John the Ripper auto-detects it as md5crypt.
How does the privilege escalation work?
User james can write to scripts in /etc/update-motd.d/, which run as root on every SSH login. Adding a SUID-bash command and then logging in via SSH yields a root shell.
Tools Used
nmap / rustscan · gobuster / dirsearch / ffuf · php · curl · netcat · hashcat / john · ssh
Educational walkthrough for the Debug room on TryHackMe. Practise responsibly — only test systems you're authorised to attack.
Tags
Keep Reading