← back to all posts

from one sql injection to a root shell on bwapp

a walk through the kill chain that anchored my capstone — a single error-based sqli that ended in a root shell via credential reuse and a webdav upload.

the most interesting findings in pentest reports usually aren't the single vulnerabilities. they're the chains — the moments where one mediocre bug feeds the next, and the next, until what started as a probing query is a shell.

this is one of those chains. it anchored my final capstone project, where the target was bWAPP (BeeBox), an intentionally vulnerable PHP/MySQL app used for OWASP Top 10 training. the scope was black-box, browser-accessible surfaces only, no DoS, medium difficulty.

what follows is the path from a URL to a root shell.

step 1 — the numeric parameter that didn't filter quotes

the endpoint was a movie-select page, sqli_2.php at medium security. the parameter was numeric: ?movie=1. sending a single quote produced a SQL error in the response — classic error-based SQLi signal.

the application used addslashes() for sanitisation. that defeats quote-based payloads like ' OR 1=1-- -, but it does nothing for numeric injection that never needs a quote in the first place.

GET /sqli_2.php?movie=0 UNION SELECT 1,2,3,4,5,6,7 -- -

seven columns. the page rendered each value where the original movie's fields had been.

step 2 — pivoting from columns to metadata

once you know the column count, the UNION becomes a window into the database. position the functions you care about in the columns the page actually renders:

GET /sqli_2.php?movie=0 UNION SELECT 1,version(),database(),user(),5,6,7 -- -

the response told me the MySQL version, the database name, and — critically — that the application was connecting as root. root-level database access from an unauthenticated request.

step 3 — extracting credentials

with root and a confirmed schema name, the users table was an easy walk:

GET /sqli_2.php?movie=0 UNION SELECT 1,login,password,email,secret,6,7 FROM users -- -

passwords stored as SHA-1 hashes. that's a finding in itself — SHA-1 is fast, unsalted, and trivially crackable for any password in a wordlist. i dumped the hashes and handed them to john the ripper.

the first crack came back almost immediately: the password was bug.

step 4 — credential reuse

a web user with a cracked password is one thing. the chain only works if those credentials are reused somewhere that matters.

phpMyAdmin was exposed. the same root / bug worked. that gave me an administrative interface to the entire database — a strong finding on its own, but in this case a stepping stone.

step 5 — webdav with listing enabled

while mapping the directory tree i'd noted a webdav path with directory listing turned on. directory listing is usually a low-severity finding. combined with write access, it's RCE waiting to happen.

i uploaded a small PHP reverse shell — the kind of file no legitimate webdav deployment should accept, sitting on a path the server would happily execute. i started a netcat listener on my kali VM:

nc -lvnp 4444

i navigated to the uploaded file. the connection came back. root shell.

the shape of the chain

pulled apart, the full chain is:

  1. error-based sqli on a numeric parameter the input filter didn't cover.
  2. union-based extraction of database metadata.
  3. extraction of users table, including hashes.
  4. offline password cracking.
  5. credential reuse against phpMyAdmin.
  6. credential reuse against webdav.
  7. php reverse shell upload.
  8. root shell.

no single step is exotic. the sqli is a textbook case. SHA-1 is a textbook weakness. webdav with listing enabled is a textbook misconfiguration. the lesson is that each one's textbook-ness is the point — defences fail in combinations that nobody designed for.

what i'd tell a developer reading this

if you're shipping php that touches a database, the three controls that would have killed this chain at different points are:

  • parameterised queries. not addslashes(), not regex filters, not "we sanitise on output". prepared statements. they make the chain impossible at step 1.
  • a real password hashing functionpassword_hash() with the default algorithm. step 3 still leaks the hashes, but step 4 becomes computationally expensive enough that the chain stalls.
  • least-privileged database users. the application does not need to connect as root. a web user with SELECT, INSERT, UPDATE, DELETE on its own schema closes step 5 entirely.

each one is a decision a developer makes once and forgets about. none of them slow down a project. all three together turn this writeup into something that ends at step 1 instead of step 8.

where it sits

the full engagement is documented in bloodmoonbreach/Bee-Box-TF — methodology, evidence captures, and the dual-audience report. the capstone scored 10/10.

more posts coming on the other vulnerability classes i covered in the same engagement: the reflected XSS that needed an <img onerror> to bypass the script-tag blacklist, and the ZAP-fuzzer setup for the broken-authentication form that rotated an anti-csrf token on every request.