Speed Chatting - TryHackMe Writeup | Unrestricted File Upload to RCE
Speed Chatting TryHackMe writeup β Love At First Breach 2026: profile picture upload, Python reverse shell, and root RCE to capture the flag.
##TryHackMe Room β Speed Chatting (Love At First Breach 2026)
A rushed Valentine messaging platform; security took a back seat.
Speed Chatter is a Valentine-themed messaging app with a profile picture upload feature. The challenge description said security had taken a back seat during development β so I prioritized file upload and injection (e.g. SSTI, path traversal). I ran a quick port scan to confirm the stack: the server was Werkzeug/Flask (Python), so PHP-based upload tricks (e.g. uploading a .php shell and hoping it runs) would not work. I tried a PHP shell anyway to confirm the file was served statically; then I pivoted to a Python reverse shell, since the backend was Python and the upload accepted any extension. I uploaded the Python shell as the profile picture; when the application later executed or imported the uploaded file (e.g. when the profile was viewed or by a background job), I received a reverse shell β and the process was running as root, so the flag was readable directly. This writeup documents the full path from recon to flag, including the dead ends (PHP, SSTI, etc.) so you can see how each step led to the next.
##Overview
| Item | Detail |
|---|---|
| Goal | Break in, get code execution, and retrieve the flag |
| Attack chain | Find upload β try PHP (fail) β pivot to Python shell β upload β trigger β root shell β flag |
| Key concepts | Unrestricted file upload, backend language matters, Python RCE |
##Reconnaissance
The application was at http://<TARGET_IP>:5000. I opened it and saw a messaging interface: a profile section, a chat room, and a profile picture upload feature. I inspected the front-end behavior: messages were loaded via /api/messages and sent via /api/send_message, and in the client code the message content was assigned with textContent (not innerHTML), so stored XSS via chat was unlikely β the app was at least escaping output there. The most promising attack surface was the file upload: if the server stored uploads in a location that was executed (e.g. as scripts or as included files), we might get RCE. First I needed to know what stack was running so I could choose the right payload. I ran a port scan with service detection:
nmap -p- -vv -sV <TARGET_IP>| Port | Service | Version |
|---|---|---|
| 22 | SSH | OpenSSH 8.9p1 |
| 5000 | HTTP | Werkzeug / Python 3.10 |
So the backend was Python (Werkzeug/Flask) β not Apache or PHP. That meant .htaccess tricks and PHP webshells would not work; the server would not execute PHP at all. Any upload-based RCE would have to be in a format the Python app could execute or import (e.g. .py if the app ever runs or imports files from the upload directory). I kept that in mind and first tested what the upload actually did with a more "classic" payload.
##Investigating file upload
The profile section accepted uploads via something like:
POST /upload_profile_pic HTTP/1.1
Content-Type: multipart/form-data
...I tried uploading a PHP reverse shell. The server responded with βProfile picture updated successfully!β and the file was reachable at a path like /uploads/profile_<uuid>.php. When I requested it, the response had:
Content-Type: application/octet-streamSo the file was served as static data (e.g. Content-Type: application/octet-stream or similar), not executed. With a Flask backend, PHP would never run anyway β the pitfall was assuming that "file upload" automatically meant "PHP shell." I had to match the language of the server: Python. Before going all-in on a Python shell, I quickly ruled out other common vectors so I didn't miss something easier: path traversal in the upload path (e.g. ../../app.py), SSTI in profile or chat fields (e.g. {{7*7}}), JSON injection, IDOR on message IDs. None of those gave execution, so the only promising path was still upload β but with a Python payload and a trigger that caused the server to execute or import the uploaded file (e.g. profile view or a cron that processes uploads).
##Eliminating other paths (brief)
I also tried path traversal, SSTI (e.g. {{7*7}}), JSON injection, IDOR on messages, etc. None gave execution. The only promising vector was upload β but the language had to match the server: Python.
##Key insight: Upload a Python shell
The upload accepted any extension and the app was Python. If uploads were stored in a directory that the app imports or executes (e.g. as part of a dynamic βthemeβ or βpluginβ load), a .py file might run. I crafted a Python reverse shell and uploaded it as the profile picture.
##Python reverse shell (concept)
I used a standard Python reverse shell (e.g. from revshells.com) that:
- >Opens a socket to my IP and port
- >Duplicates stdin/stdout/stderr to the socket
- >Spawns a shell (e.g.
pty.spawn("sh"))
I saved it as shell.py, started a listener:
rlwrap nc -lnvp 1337then uploaded shell.py as the profile picture. The server stored it; when the app executed or evaluated the uploaded file (e.g. on next profile view or a cron), I received a connection and got a root shell (uid=0). So the app was running as root and executed the uploaded Python file.
##Capturing the flag
The first shell sometimes dropped (e.g. due to request lifecycle). I ran the listener again, re-triggered the upload (or the action that caused execution), and in the new shell ran:
cat flag.txtThe output was the challenge flag in THM{...} format.
##Flag
THM{*redacted*}(Replace with the actual flag when you solve the room.)
##Attack chain summary
| Step | Vulnerability / action | Impact |
|---|---|---|
| 1 | Unrestricted file upload | Arbitrary file placement |
| 2 | Python file allowed + execution in app context | Backend code execution |
| 3 | No privilege separation | Root shell |
| 4 | Flag file readable | Full compromise |
##Pitfalls and notes
- >PHP-first mindset: Wasted time on PHP shells and
.htaccess; the stack was Python. - >Giving up on upload after PHP failed: The same upload endpoint accepted Python; the missing piece was language and execution context, not upload itself.
- >Real-world: Validate extension and content, store uploads outside webroot or execution paths, run the app as a non-root user, and avoid executing or importing user-uploaded files.
##References and tools
- >TryHackMe β Love At First Breach
- >RevShells β reverse shell payloads
- >OWASP β Unrestricted File Upload
This writeup is part of my Love At First Breach 2026 event writeups.