The safe practice ground
You do not learn to pick locks on someone else's front door. DVWA — the Damn Vulnerable Web Application — is a deliberately insecure PHP/MySQL web app whose entire purpose is to give you a legal target you own, so you can practice real attacks without touching anything that matters. You stand it up on your own machine, you break it, and the only thing you damage is a throwaway lab.
What makes it a teaching tool rather than just a broken app is the security-level switch. Every vulnerability in DVWA ships at four settings — Low, Medium, High, and Impossible — and you can watch the same defense get progressively stronger. At Low there is essentially no protection; at Medium a naive filter appears; at High the filter tightens; at Impossible the code is written the way production code should be, and the attack simply stops working. Climbing that ladder teaches you both how an attack succeeds and exactly which control finally shuts it down.
Why chaining matters
A single vulnerability, taken alone, is often disappointing. An upload form that accepts the wrong file type might let you store a file the server never runs. A page that includes files based on a parameter might only let you read text you were not supposed to see. On their own, each is a medium finding — annoying, worth fixing, rarely catastrophic.
The danger is in the chain. Attackers do not look for one perfect bug; they look for a sequence of imperfect ones where each weakness hands the next attack its opening. The upload flaw gets a malicious file onto the server. The inclusion flaw gets the server to run it. Neither would be game-ending alone, but stitched together they turn two medium problems into the worst outcome a web application can suffer.
This lesson walks that exact path — from a file upload to full remote code execution — and at each step calls out the weak control that made the next move possible. Learn to see the seams between bugs, because that is where real compromise lives.
Vulnerability 1 — insecure file upload
An upload form is supposed to accept something inert — a profile picture, a PDF, a spreadsheet. An insecure file upload is one that fails to properly verify what it is actually receiving, so an attacker can hand it a server-side script instead of an image. The most dangerous version is a small piece of PHP — a web shell — a file that, if the server ever executes it, runs whatever commands the attacker passes in.
The reason this works is that the checks developers reach for first are the ones easiest to defeat:
- Client-side validation — a check that runs in the browser before upload. It is purely cosmetic: an attacker who controls their own request simply skips the browser and sends the file directly, so this stops honest users and nobody else.
- Trusting the content-type header — the browser says a file is an image by sending a Content-Type: image/png header. That header is attacker-controlled text, not proof; it can be set to anything while the file underneath is a script.
- Extension blocklists — banning .php looks reasonable until you remember the list is never complete. Alternate executable extensions, case tricks, and double extensions slip past a list of "bad" names because a blocklist can only block what its author thought to forbid.
The fix flips every one of those around. Allow-list the handful of types you actually intend to accept rather than trying to enumerate the bad ones; verify the file's real content, not its claimed header; rename uploads to a server-generated name so the attacker never controls the filename; and store uploads outside the web root so even a malicious file cannot be reached and executed through a URL. The principle underneath all four: never let the user decide what gets written, where it lands, or what it is called.
Vulnerability 2 — local file inclusion, and the chain to RCE
A local file inclusion (LFI) flaw appears when a parameter controls which file the server includes and runs. Picture a page that builds its content from a URL parameter — something like ?page=home — by pasting that value straight into a file-include function. If the application trusts the parameter, an attacker can rewrite it to point somewhere it was never meant to go.
The first thing that buys an attacker is path traversal: walking up the directory tree with repeated ../ sequences to read files outside the intended folder. A request like ?page=../../../../etc/passwd climbs to the system root and reads a file that lists the server's user accounts — a classic proof that the parameter is fully attacker-controlled.
Reading /etc/passwd is a leak. The real prize is the chain. Bring the upload flaw back: first you abuse the insecure upload to plant a web shell somewhere on the server's disk. The upload alone is inert — the file just sits there. Then you point the LFI parameter at the file you just uploaded. Because an inclusion function does not merely read a PHP file, it executes it, the server now runs your code. Two medium-severity bugs have just combined into RCE — remote code execution, the highest-impact outcome a web application can hand an attacker, because from there the attacker is no longer talking to the app; they are running commands on the machine.
| Step | Weakness exploited | Result |
|---|---|---|
| 1 · Plant | Insecure file upload accepts a server-side script disguised as a harmless file | A web shell now sits on the server's disk — present, but not yet running. |
| 2 · Locate | Path traversal via the LFI parameter ?page=../../ confirms attacker control over which file loads | Attacker reads files like /etc/passwd and pins down where the uploaded shell lives. |
| 3 · Execute | LFI points the include function at the uploaded web shell, which the server runs instead of merely reading | RCE — arbitrary commands run on the server with the web application's privileges. |
Read the table top to bottom and the lesson is plain: no row is fatal by itself, but each one removes an obstacle for the next. That is the anatomy of nearly every serious web compromise — not one spectacular bug, but a quiet relay of small ones.
The defensive flip and where to go deeper
Every step in that chain has a control that would have broken it, and a defender does not need to win at every layer — cutting any one link stops the whole sequence. Carry these forward:
- Validate and allow-list uploads. Accept only the specific types you intend to handle, check real content rather than headers, rename files to a server-chosen name, and store them outside the web root so a planted file can never be reached as a URL.
- Never feed user input to file-include functions. If a page must choose between files, map a parameter to a fixed internal allow-list of known-good pages — the user picks an option, never a path. That single rule eliminates both path traversal and the inclusion-based execution it enables.
- Run least-privileged. If the web process runs with only the permissions it truly needs, a shell that does land is contained — it cannot read the rest of the system, pivot, or persist nearly as freely.
- Add a WAF as defense-in-depth. A web application firewall is not a substitute for fixing the code, but as an outer layer it can catch traversal sequences and known shell patterns and buy time — valuable precisely because it sits in front of mistakes the code missed.
To turn this walkthrough into muscle memory, work the full hands-on labs on the site, where each weakness gets its own guided range: file upload, path traversal, and command injection are covered in the web security labs, and database-side compromise gets its own dedicated track in the SQL injection labs. Reading the chain once teaches you the shape; running it yourself teaches you the timing.
Next, the track leaves the web stack behind entirely and steps into the air around it — Wireless Attacks, where the target is no longer an application but the radio link that carries it.