July 29, 2026
From SQLi to Full Server Takeover ๐ฏ A Web App CTF Breakdown
How a single injection point unraveled into admin credentials, remote code execution, and a fully compromised server.
By Mohamed Mahmoud Metwally
4 min read
๐ Intro
I recently worked through a grey-box web application CTF hosted atctf.u16.online, and it turned out to be a great chained-exploitation exercise. What started as a simple SQL injection in a search bar ended with a reverse shell on the box. ๐
In this writeup, I'll walk through each vulnerability I found, how I chained them together, and what the underlying root causes were โ five findings in total, ranging from Insecure Direct Object References all the way up to unauthenticated-to-root-shell RCE.
Stack rank of what's coming:
- ๐ Union-based SQL Injection โ leaked admin creds
- ๐ค Unrestricted File Upload โ Remote Code Execution
- ๐ฅ๏ธ OS Command Injection โ 3 separate entry points
- ๐ Command execution via an uploaded web shell
- ๐ IDOR โ read any user's profile, including the admin's
Let's get into it.
๐ 1. Union-Based SQL Injection
Severity: High | CVSS 8.8
The post search feature (/posts/search?q=) passed my input straight into a SQL query with no sanitization. Classic setup for a UNION-based injection.
First step โ figure out the table layout:
' union select 1, group_concat(table_name), 3, 4 from information_schema.tables where table_schema = database() and 1=1 -- -' union select 1, group_concat(table_name), 3, 4 from information_schema.tables where table_schema = database() and 1=1 -- -Then the columns:
' union select 1, group_concat(column_name), 3, 4 from information_schema.columns where table_schema = database() and 1=1 -- -' union select 1, group_concat(column_name), 3, 4 from information_schema.columns where table_schema = database() and 1=1 -- -
Once I had the schema mapped out, I pulled the CTF flag and the administrator's stored credentials directly from the database. ๐๏ธ
That admin password unlocked everything else in this write-up.
๐ค 2. Remote Code Execution via Unrestricted File Upload
Severity: Critical | CVSS 10.0
Logged into the admin panel with the creds from the SQLi. ๐ Straight away I found a file upload endpoint with zero validation โ no extension check, no MIME check, no content inspection. Nothing.
So naturally, I uploaded a PHP reverse shell and hit it directly in the browser.
nc -lvnp 4444nc -lvnp 4444Got a callback immediately:
Full remote code execution, no further exploitation required. This one finding alone would justify a Critical rating โ an unauthenticated file-type check is about as bad as it gets. โ ๏ธ
๐ฅ๏ธ 3. OS Command Injection โ Three Different Entry Points
Severity: Critical | CVSS 9.8
While poking around the admin panel's "System Health Checks" page, I noticed it had a denylist blocking commands like cat, ls, wget, curl, nc, bash, sh, and exec. ๐ซ
Cute โ but the ; command separator was never filtered. So I just chained a different command after it.
Ping tool:
8.8.8.8; more /etc/passwd8.8.8.8; more /etc/passwd
API health check + database host field, same trick:
api:5000/health;cat /etc/passwd
db;cat /etc/passwd;api:5000/health;cat /etc/passwd
db;cat /etc/passwd;
Denylists are a losing game โ all it takes is one unblocked metacharacter and the whole filter is irrelevant. ๐งจ
๐ 4. Command Execution via an Uploaded Web Shell
Severity: High | CVSS 8.6
(Originally logged as "LFI" โ but on review this isn't file inclusion at all, it's straightforward command execution through a parameter on a web shell I'd already uploaded. Worth calling out, since mislabeling a finding can undersell โ or oversell โ the actual risk.)
Using the same broken upload functionality from Finding 2, I dropped a small PHP web shell that accepts commands via a cmd parameter:
/uploads/webshell.php?cmd=id/uploads/webshell.php?cmd=id
Running as www-data, but still full command execution on demand โ no reverse shell needed for quick recon.
๐ 5. IDOR on the Profile Endpoint
Severity: High | CVSS 7.5
Last one โ the profile endpoint trusted a client-supplied user_id with no ownership check whatsoever.
GET /profile?user_id=3GET /profile?user_id=3
Swapping the ID handed back another user's full profile โ email, username, and an is_admin flag โ no authorization check in sight. A textbook IDOR. ๐ณ๏ธ
๐ง Root Cause & Takeaways
Zooming out, every one of these findings traces back to the same underlying habit: trusting client input.
- ๐ซ No parameterized queries โ SQL injection
- ๐ซ No file-type/content validation โ arbitrary upload โ RCE
- ๐ซ Denylist instead of allowlist โ command injection bypass
- ๐ซ No server-side ownership check โ IDOR
And they chain beautifully for an attacker: SQLi โ admin creds โ file upload โ RCE. One weak link at the front of the chain is often enough to compromise the whole app.
Fixes that would've stopped this dead:
- โ Parameterized/prepared statements everywhere
- โ Allowlist file validation (extension + MIME + magic bytes) + no execute permissions on upload dirs
- โ Never pass user input to a shell โ use native APIs instead of shelling out
- โ Server-side authorization checks on every object reference
โ๏ธ About Me
I'm Mohamed Mahmoud Metwally, a Computers & Control Systems Engineering graduate and aspiring penetration tester, currently building out my offensive security portfolio. If you enjoyed this breakdown, I write these up as I work through CTFs and labs โ feel free to follow along. ๐
Thanks for reading!