InmoHogar Roomshare CTF — Write-Up
Yo, so this box was pretty fun. Docker container, 2 ports open, and a whole chain: APK → API → SSH → root. Let me walk through it.
Target: 172.17.0.2
Attacker: Parrot OS
Final flag: FLAG{stage4_root_270651efba41_1791925049}
1. Nmap
First thing, scanned the whole port range:
Got:
Just SSH and HTTP. Nothing fancy. Let's poke the web.
2. Web Enumeration
Hit the root:
nginx 1.18.0, 1875-byte HTML page. Cool, some kind of booking app.
Time for gobuster:
Got two interesting things:
Also noticed this in the HTML:
A whole APK just sitting there. Yeah, I'm gonna download that later.
Checking /booking
JSON response. So it's an API, not a webpage.
Let me try all methods:
Results:
| Method | Status | Response |
|---|---|---|
| GET | 404 | {"error": "not found"} |
| POST | 401 | {"status": 401, "message": "missing X-API-Key"} |
| PUT | 501 | Python http.server error |
| DELETE | 501 | same |
| OPTIONS | 501 | same |
Boom. POST exists, needs X-API-Key header. And those 501 errors are from Python's built-in http.server behind nginx. Nice to know.
3. Stage 1 — APK Decompiling
Downloaded the APK:
Decompiled with jadx:
Grepped for secrets:
Found gold in BuildConfig.java:
🚩 Stage 1:
FLAG{stage1_buildconfig_leak_BKGVlytS}
Got the API key: BKGVlytSjDihCgXmNPm1cM6QwhRjrT86yJ2sEaf9
4. Stage 2 — Hitting the API
Tried the key on POST:
Got 404 {"error": "not found"}.
Weird, right? But this actually tells us something. Before, with no key, we got 401 missing X-API-Key (47 bytes). With a bad key, we got 401 invalid key (41 bytes). Now with the right key, we get 404 not found (22 bytes). So the key is valid — we just need to find the right route.
Fuzzing the API
Wrote a Python script that hammered the API with thousands of routes, headers, bodies. 60 threads.
After a while:
Response:
🚩 Stage 2 done. SSH creds:
deploy/c0mpartir-p1s0s
Why didn't the browser show this? Because the browser always sends GET, and this route only takes POST. Also, no browser sends X-API-Key by default. Always use curl for APIs.
5. Stage 3 — SSH
sshpass -p 'c0mpartir-p1s0s' ssh -o StrictHostKeyChecking=no [email protected]
|
Logged in as deploy@270651efba41.
🚩 Stage 3:
stage3_ssh_user_land_270651efba41
So now I'm a low-priv user. Time to root.
6. Stage 4 — PrivEsc (the sudo part)
First thing you always do after landing on a box:
Output:
Two things jump out:
manage-svc— runs as root, no password required.restart-booking-gw— also NOPASSWD, but the file doesn't exist (checked withls). Potential path hijack, but let's trymanage-svcfirst.
Looking at manage-svc
It's an ELF 64-bit binary. Before running it, let's see what strings it has:
The interesting ones:
Read it slowly. The binary:
- Calls
geteuid()— checks if it's SUID. - If not, refuses to run.
- If yes, calls
setuid(0)+setgid(0)→ root. - Then runs something like:
So it takes the action argument and runs it as root via bash -c. That's the whole vuln. It's a sudo-allowed binary that spawns a root shell with whatever command you give it.
Popping root
You could just pass bash and get a root shell:
Output:
Root. 🎉
Note: the "usage" line and "restart: command not found" are just noise from the binary's logger before it actually runs the action. Ignore them.
Alternative one-shot (no shell)
If you don't want a shell, you can just run any command:
Grabbing the final flag
🚩 Stage 4 (final):
FLAG{stage4_root_270651efba41_1791925049}
Why this worked (the "why")
The binary has two security problems stacked on top of each other:
- It's SUID root — anyone who runs it gets
euid=0inside the process. - It runs
bash -c <arg>after dropping into root — meaning any string you pass gets executed as root.
Combined with sudo -l allowing deploy to run it without a password, it's a straight shot to root. Even without sudo, SUID alone would be enough — the binary explicitly checks geteuid() and only proceeds if it's running SUID.
This is a textbook GTFOBins-style escalation. Same idea as sudo vim, sudo find, sudo python3 — anything that lets you spawn a shell or run arbitrary code while privileged.
7. Chain Recap
| Stage | What | Flag |
|---|---|---|
| 1 | APK → BuildConfig leak | FLAG{stage1_buildconfig_leak_BKGVlytS} |
| 2 | POST /booking/api/sessions with API key |
→ SSH creds |
| 3 | SSH as deploy | stage3_ssh_user_land_270651efba41 |
| 4 | sudo manage-svc bash → root |
FLAG{stage4_root_270651efba41_1791925049} |
8. How to Fix This Stuff
If I were a dev on this app:
- Don't hardcode API keys in the APK. Anyone can decompile it in 2 minutes. Fetch secrets from a server after auth.
- Don't return different errors for "missing key" vs "invalid key". Just say "unauthorized" for both. Otherwise you're telling attackers which keys are close.
- Rate-limit the API. Fuzzing found that route in seconds with 60 threads.
- Strong SSH passwords. Or better, SSH keys only, disable password auth.
- Don't write SUID binaries that run
bash -c <user-input>as root. That's basically handing out root. Whitelist commands, or better, don't use a shell at all. - Don't
sudo NOPASSWDon anything that spawns a shell.manage-svcshould not have been allowed.
9. Tools I Used
nmap— port scangobuster— dir brute forcecurl— manual API callsjadx— APK decompilation- Python +
requests+ ThreadPoolExecutor — custom aggressive fuzzer sshpass— SSH loginstrings,sudo -l— Linux enum
10. Final Thoughts
Cool box. Chain made sense — each stage gave you exactly what you needed for the next. The tricky part was Stage 2 because you had to figure out:
- The correct method (POST, not GET)
- The correct header (
X-API-Key) - The correct route (
/booking/api/sessions)
Once you had the API key from the APK, it was just a matter of fuzzing until something different popped up. Everything after that was pretty standard — SSH with leaked creds, then a badly-written SUID binary for root.
4/4 flags. Box owned. 🏆
by: Break — 2026-10-14 — DockerLabs