Writeups/TryHackMe/Speed Chatting - TryHackMe Writeup | Unrestricted File Upload to RCE
TryHackMeEasyRoom

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

ItemDetail
GoalBreak in, get code execution, and retrieve the flag
Attack chainFind upload β†’ try PHP (fail) β†’ pivot to Python shell β†’ upload β†’ trigger β†’ root shell β†’ flag
Key conceptsUnrestricted 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:

bash
nmap -p- -vv -sV <TARGET_IP>
PortServiceVersion
22SSHOpenSSH 8.9p1
5000HTTPWerkzeug / 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:

http
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:

text
Content-Type: application/octet-stream

So 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:

bash
rlwrap nc -lnvp 1337

then 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:

bash
cat flag.txt

The output was the challenge flag in THM{...} format.


##Flag

text
THM{*redacted*}

(Replace with the actual flag when you solve the room.)


##Attack chain summary

StepVulnerability / actionImpact
1Unrestricted file uploadArbitrary file placement
2Python file allowed + execution in app contextBackend code execution
3No privilege separationRoot shell
4Flag file readableFull 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


This writeup is part of my Love At First Breach 2026 event writeups.

$ echo "Open to Red Team Security Research and Security Engineering roles."

> Open to Red Team Security Research and Security Engineering roles.

$ uptime

> Portfolio online since 2024 | Last updated: Mar 2026

"No one is useless in this world who lightens the burdens of another." β€” Charles Dickens

Considered a small donation if you found any of the walkthrough or blog posts helpful. Much appreciate :)

Buy me a coffee

Β© 2026 Shivang Tiwari. Built with Next.js. Hack the planet.