Exploit SQL Injection Using DVWA
Hack a vulnerable web app using SQL injection techniques from CTFs.
Introduction
30 Second Summary
Every time you log in to a website, your username travels into a database query behind the scenes. SQL injection is what happens when an attacker slips their own commands into that query.
In this project, you will set up a local hacking lab and exploit it using the same SQL injection techniques found in CTF competitions and real penetration tests.
What You'll Build
A fully functional SQL injection attack lab running on your local machine using Docker and DVWA (Damn Vulnerable Web Application).
By the end of this project, you'll have:
- A reusable Docker-based hacking lab with DVWA and a MariaDB database.
- Extracted database credentials (usernames and password hashes) using UNION-based SQL injection.
- Mastered blind boolean-based injection to extract hidden data one character at a time.
- Secret Mission: Bypass security filters by intercepting and modifying HTTP requests using browser Developer Tools.
Are there any prerequisites?
You should be comfortable with basic SQL concepts like SELECT, WHERE, and AND/OR operators. No prior security or hacking experience is needed.
Set Up Your Hacking Lab
Before you can practice SQL injection, you need a safe target to attack. Practicing on real websites is illegal, so you need a local lab environment designed to be exploited.
In this step, you will launch DVWA (Damn Vulnerable Web Application) inside Docker on your machine. DVWA is a deliberately vulnerable web app used by security professionals and CTF players to practice common exploits.
In this step, get ready to:
- Launch DVWA and its database using Docker Compose.
- Initialize the database and log in to DVWA.
- Set the security level to Low for unfiltered practice.
Create your Docker Compose file and launch DVWA
DVWA runs as two containers: the web application itself and a MariaDB database that stores the vulnerable data you will exploit. Docker Compose lets you define and launch both containers together with a single command.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir dvwa-lab
cd dvwa-lab
- Confirm the folder exists by running:
ls
You should see the dvwa-lab folder listed in the output.
Now you need to create the docker-compose.yml file that defines your two-container lab.
- Create the file by running:
touch docker-compose.yml
- Open docker-compose.yml in a text editor of your choice and paste the following configuration:
services:
dvwa:
image: ghcr.io/digininja/dvwa:latest
pull_policy: always
environment:
- DB_SERVER=db
depends_on:
- db
networks:
- dvwa
ports:
- 127.0.0.1:4280:80
restart: unless-stopped
db:
image: mariadb:10
environment:
- MYSQL_ROOT_PASSWORD=dvwa
- MYSQL_DATABASE=dvwa
- MYSQL_USER=dvwa
- MYSQL_PASSWORD=p@ssw0rd
volumes:
- dvwa:/var/lib/mysql
networks:
- dvwa
restart: unless-stopped
networks:
dvwa:
volumes:
dvwa:
What does this configuration do?
The dvwa service pulls the official DVWA image from GitHub Container Registry and maps port 4280 on your machine to port 80 inside the container.
The db service runs MariaDB with pre-configured credentials that DVWA uses to connect to its database. The 127.0.0.1 binding ensures DVWA is only accessible from your machine, not the network.
- Save docker-compose.yml.
- Launch both containers by running:
docker compose up -d
Docker will pull the DVWA and MariaDB images, then start both containers in the background. This may take a minute on the first run while the images download.
- Verify both containers are running by checking:
docker compose ps
You should see two services listed with a status of Up.
Containers not starting?
- Make sure Docker Desktop is running. You can check by looking for the Docker whale icon in your system tray or menu bar.
- If you see a port conflict error, another service might be using port 4280. Stop the other service or change the port in your compose file.
- help me debug Docker Compose errors when launching DVWA
✔️ Awesome, I've got everything!
Great. Make sure you have saved docker-compose.yml and both containers show Up in the docker compose ps output.
ⓧ I'd like to double check the full code
Here is the complete docker-compose.yml file:
services:
dvwa:
image: ghcr.io/digininja/dvwa:latest
pull_policy: always
environment:
- DB_SERVER=db
depends_on:
- db
networks:
- dvwa
ports:
- 127.0.0.1:4280:80
restart: unless-stopped
db:
image: mariadb:10
environment:
- MYSQL_ROOT_PASSWORD=dvwa
- MYSQL_DATABASE=dvwa
- MYSQL_USER=dvwa
- MYSQL_PASSWORD=p@ssw0rd
volumes:
- dvwa:/var/lib/mysql
networks:
- dvwa
restart: unless-stopped
networks:
dvwa:
volumes:
dvwa:
Initialize DVWA and log in
The containers are running, but DVWA needs a one-time database initialization before you can use it.
- Open your web browser (Chrome or Firefox).
- Navigate to http://localhost:4280 in the address bar.
- You will see the DVWA login page. Enter admin as the username.
- Enter password as the password.
- Click Login.
You are now on the DVWA home page. Next, initialize the database.
- Click Setup / Reset DB in the left menu.
- Scroll to the bottom of the page and click the Create / Reset Database button.
DVWA will create the database tables and seed them with test users. You will be redirected to the login page.
- Log in again with username admin and password password.
Why do you need to log in twice?
The database reset clears all session data, so DVWA logs you out automatically. This is expected behavior.
Page not loading?
- Wait 10-15 seconds after running docker compose up -d. The MariaDB container needs time to initialize on first launch.
- Make sure you are using http:// not https:// in the URL.
- help me access DVWA in my browser after running docker compose
Set the security level to Low
DVWA has four security levels: Low, Medium, High, and Impossible. Each level adds progressively stronger input filtering. The Docker image defaults to Impossible, which blocks all injection attempts.
- Click DVWA Security in the left menu.
- In the Security Level dropdown, select Low.
- Click Submit.
The page will confirm the security level is now set to Low. You can verify this in the bottom-left of any DVWA page where it displays the current level.
Why start at Low?
At Low security, DVWA applies zero input filtering. This lets you focus on learning how SQL injection works without dealing with filters or encoding tricks. Once you understand the core technique, the Secret Mission will challenge you to bypass Medium-level protections.
Security level not changing?
- Make sure you clicked Submit after selecting Low from the dropdown.
- Check the bottom-left corner of the page. It should display "Security Level: low".
- help me change the DVWA security level to Low
Your hacking lab is ready. Next up, you will use this lab to perform your first SQL injection attack and dump all user records from the database.
Break In with Basic SQL Injection
Your hacking lab is running and DVWA is ready to be exploited. The first technique you will learn is the most fundamental SQL injection attack: tricking an application into running SQL you control by manipulating its input fields.
DVWA has a module that looks up user records by ID. Behind the scenes it builds a SQL query using whatever you type. If the application trusts your input blindly, you can break out of the intended query and make the database do whatever you want.
In this step, get ready to:
- Test the SQL Injection module with normal input to see how the application behaves.
- Inject a payload that forces the database to return every user record.
- Understand how the injection breaks the original SQL query.
Navigate to the SQL Injection module
DVWA organizes its vulnerabilities into separate modules, each targeting a different type of attack. You need to open the SQL Injection module to access the vulnerable input form.
- In your browser, make sure you are on DVWA at http://localhost:4280.
- In the left sidebar, click SQL Injection.
You should see a simple form with a text field labeled User ID: and a Submit button.
What is this form doing?
This form takes a user ID number and queries the database to look up that person's name. Behind the scenes, the PHP code builds a SQL query by inserting your input directly into the query string with no filtering or validation.
Test normal input first
Before attempting any injection, observe how the application responds to a legitimate input. This gives you a baseline to compare against when your payloads start changing the behavior.
- Type 1 into the User ID field.
- Click Submit.
You should see a single result showing the ID, First name, and Surname for user 1 (admin).
What query ran behind the scenes?
When you submitted 1, the server built this SQL query:
SELECT first_name, last_name FROM users WHERE user_id = '1';
Your input was dropped straight into the query between the single quotes. The database found the one row matching user_id 1 and returned its first_name and last_name columns.
Inject a payload to dump all users
Now you will craft input that breaks out of the intended query structure. Instead of returning one user, your payload will force the database to return every record in the table.
- Clear the User ID field.
- Type the following payload into the field exactly as shown:
1' OR '1'='1' -- -
- Click Submit.
You should see multiple results appear on the page. Every user in the database is now listed with their ID, First name, and Surname. You just dumped the entire users table with a single input.
Only seeing one result or a blank page?
Make sure you copied the payload exactly, including the single quotes and the space between -- and -. The space after the two dashes is required for MySQL to recognize it as a comment.
Also confirm that your DVWA security level is set to Low. Check the bottom-left corner of the page where it displays the current security level.
Help me figure out why my SQL injection payload is not working in DVWA.
Understand why the injection works
Seeing all the records appear is exciting, but understanding the mechanics is what makes you dangerous. Let's break down exactly what happened to the SQL query when you submitted that payload.
How did the payload change the query?
The original query template looks like this:
SELECT first_name, last_name FROM users WHERE user_id = '$id';
When you typed 1' OR '1'='1' -- -, the server inserted it directly into the query, producing:
SELECT first_name, last_name FROM users WHERE user_id = '1' OR '1'='1' -- -';
Three things happened. First, the ' after 1 closed the original string early, breaking out of the intended input boundary. Second, OR '1'='1' added a condition that is always true, so the WHERE clause matches every row. Third, -- - is a SQL comment that neutralizes the leftover closing quote from the original query.
Why does this matter in the real world?
This exact pattern appears in CTF web challenges and real penetration tests. Any application that concatenates user input directly into SQL queries is vulnerable. The fix is parameterized queries (also called prepared statements), which treat input as data rather than executable code.
You just performed your first successful SQL injection. You proved you can manipulate the query, but so far you have only seen first names and last names. Next up, you will use a more advanced technique called UNION injection to extract any data from any table in the database.
Extract Database Secrets with UNION Injection
In the previous step, you tricked the application into revealing all its user records. That was powerful, but the data you saw was limited to first names and surnames. The real prize in any database is the credentials table.
In this step, you will use UNION injection to attach your own queries to the application's original query. This lets you pull data from any table in the database, not just the one the developer intended.
In this step, get ready to:
- Determine how many columns the original query returns.
- Enumerate database tables using information_schema.
- Extract usernames and password hashes from the users table.
Determine the column count with ORDER BY
A UNION statement in SQL combines the results of two SELECT queries into one output. There is one strict rule: both queries must return the same number of columns.
Before you can inject a UNION query, you need to figure out how many columns the original query returns. The ORDER BY clause lets you sort results by column number. If you sort by a column that does not exist, the database throws an error.
- Navigate to the SQL Injection module in the DVWA sidebar (the same page you used in Step 2).
- In the User ID field, type this payload and click Submit:
1' ORDER BY 1-- -
You should see the results for user ID 1 displayed normally. The query succeeded because column 1 exists.
- Now try sorting by column 2. Type this payload and click Submit:
1' ORDER BY 2-- -
Results appear again. Column 2 exists too.
- Now try column 3. Type this payload and click Submit:
1' ORDER BY 3-- -
This time you should see an error instead of results. The query failed because there is no column 3.
What did you just learn?
ORDER BY 1 worked. ORDER BY 2 worked. ORDER BY 3 failed. This tells you the original query returns exactly 2 columns.
Now you know your UNION payloads must also select exactly 2 columns to match.
- Confirm UNION compatibility by typing this payload and clicking Submit:
1' UNION SELECT null,null-- -
You should see the normal results for user ID 1, plus an extra row with empty values. That extra row is your injected UNION query returning two null values. UNION works.
Seeing an error on the UNION query?
Make sure you typed exactly two null values separated by a comma. The column count must match the original query (2 columns).
Check that the comment syntax -- - has a space between the two dashes and the hyphen. Without the space, MySQL does not recognize it as a comment.
Also verify your security level is still set to Low in DVWA Security.
help me fix my UNION SELECT payload in DVWA
Enumerate databases and tables with UNION SELECT
Every MySQL and MariaDB server has a built-in database called information_schema. It stores metadata about every database, table, and column on the server. Think of it as the database's table of contents.
- In the User ID field, type this payload to list all tables across all databases, then click Submit:
1' UNION SELECT table_schema,table_name FROM information_schema.tables-- -
A long list of results appears. Each row shows a database name under First name and a table name under Surname.
What does this payload do?
- table_schema is the database name. It maps to the first column, which DVWA displays as "First name".
- table_name is the table name. It maps to the second column, which DVWA displays as "Surname".
- information_schema.tables is the system table that catalogs every table on the server.
Scroll through the results and look for entries where the First name column shows dvwa. You should spot a table called users. That is your target.
Narrowing your search
The results list is long because it includes system tables from information_schema itself. In a real engagement, you would add a WHERE clause to filter by database name. For now, scrolling through the list is enough to find the dvwa.users table.
Dump the users table
You now know the database is called dvwa and it contains a table called users. Time to extract credentials from it.
- In the User ID field, type this payload and click Submit:
1' UNION SELECT user,password FROM dvwa.users-- -
You should see each user's login name under First name and their password hash under Surname. The hashes are 32-character MD5 strings.
What just happened?
You told the database to select the user and password columns from dvwa.users. Because the original query returns 2 columns and your UNION also selects 2 columns, the database happily combined the results.
The application never intended to show you password hashes. It only meant to display first and last names. But because the input was not sanitized, you rewrote the query to pull from a completely different table.
Not seeing password hashes?
Make sure you typed dvwa.users (with the database prefix). Without the dvwa. prefix, the database may not find the table.
Check that the column names are exactly user and password (lowercase, no quotes around them).
help me troubleshoot my UNION SELECT for extracting DVWA user credentials
You just compromised the entire user database. In a real penetration test, the next step would be cracking those MD5 hashes offline. Next up, you will tackle blind SQL injection, where the application hides all query output and you have to extract data one character at a time.
Master Blind SQL Injection
In the previous step, you extracted full database contents because the application helpfully displayed query results on screen. Real-world applications rarely do this.
In this step, you will tackle blind SQL injection. The application only tells you whether a record exists or not. No names, no hashes, no data at all. You will extract secrets one character at a time using true/false questions.
In this step, get ready to:
- Observe how the Blind SQL Injection module hides all query output.
- Extract the database version character-by-character using boolean payloads.
- Use binary search logic to efficiently extract a username from the users table.
Navigate to the Blind SQL Injection module
The Blind SQL Injection module uses the same vulnerable query as the regular module. The difference is that it never shows you the query results.
- In the DVWA navigation menu on the left, click SQL Injection (Blind).
- Type 1 into the User ID field and click Submit.
You should see the message: "User ID exists in the database." No first name, no surname. Just a yes/no answer.
- Now type 999 into the field and click Submit.
You should see: "User ID is MISSING from the database." These two responses are the only feedback you get.
What is blind SQL injection?
Blind SQL injection is when a vulnerable application does not display database output to the attacker. The attacker can still inject SQL, but they can only observe whether the query returned results ("exists") or not ("MISSING").
To extract data, the attacker asks the database true/false questions. For example: "Is the first character of the version string equal to 'a'?" If the app says "exists," the answer is yes. If it says "MISSING," the answer is no.
Extract the database version one character at a time
You will now confirm that boolean injection works by testing a known true and known false condition. Then you will use the SUBSTRING function to pull out the database version one character at a time.
- Type this payload into the User ID field and click Submit:
1' AND 1=1-- -
You should see "User ID exists in the database." because both conditions are true (user 1 exists AND 1 equals 1).
- Now try the false version. Type this payload and click Submit:
1' AND 1=2-- -
You should see "User ID is MISSING from the database." because 1 does not equal 2, making the overall condition false.
What does this prove?
The app is evaluating your injected conditions. When your condition is true, it returns "exists." When false, it returns "MISSING." You now have a reliable true/false oracle you can ask any question.
Now you will use SUBSTRING to ask: "What is the first character of the database version?"
- Type this payload and click Submit:
1' AND SUBSTRING(@@version,1,1)='5'-- -
You should see "User ID is MISSING from the database." This tells you the first character of the version is NOT '5'.
- Try a different guess. Type this payload and click Submit:
1' AND SUBSTRING(@@version,1,1)='1'-- -
You should see "User ID exists in the database." The first character of the version string is '1'. This makes sense because your Docker container runs MariaDB 10, whose version string starts with "10".
- Confirm the second character by typing this payload and clicking Submit:
1' AND SUBSTRING(@@version,2,1)='0'-- -
You should see "User ID exists in the database." The second character is '0', confirming the version starts with "10".
How does SUBSTRING work here?
SUBSTRING(@@version,1,1) extracts 1 character from position 1 of the @@version system variable. By changing the position number, you walk through the string one character at a time.
The -- - at the end is a MySQL/MariaDB comment that discards the rest of the original query. The space and extra character after -- are required for MySQL to recognize it as a comment.
Seeing 'MISSING' for everything?
Make sure you include the space between -- and - at the end of the payload. Without that space, MySQL does not treat it as a comment.
Also check that you are using straight single quotes (') and not curly quotes. Some browsers or text editors auto-correct quotes.
Still stuck? Why is my blind SQL injection payload always returning MISSING?
Extract a username using binary search
Guessing one character at a time by trying every letter is slow. There are 36 possible characters (a-z plus 0-9) for each position. Binary search cuts this down dramatically by asking "is it greater than M?" to eliminate half the alphabet in one query.
- Start by asking if the first character of the first username is greater than 'm'. Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),1,1)>'m'-- -
You should see "User ID is MISSING from the database." This means the first character is 'm' or earlier in the alphabet.
- Narrow it further. Is it greater than 'f'? Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),1,1)>'f'-- -
You should see "User ID is MISSING from the database." The character is 'f' or earlier.
- Is it greater than 'c'? Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),1,1)>'c'-- -
You should see "User ID is MISSING from the database." The character is 'c' or earlier.
- Is it greater than 'a'? Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),1,1)>'a'-- -
You should see "User ID is MISSING from the database." The character is NOT greater than 'a'. That means it IS 'a'.
- Confirm by testing equality. Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),1,1)='a'-- -
You should see "User ID exists in the database." The first character of the first username is confirmed as 'a'.
Why binary search?
With 26 letters, guessing one at a time takes up to 26 queries per character. Binary search takes at most 5 queries per character because each query eliminates half the remaining possibilities.
In CTF competitions, speed matters. Automated scripts use this exact binary search technique to extract entire databases through blind injection in seconds.
You can continue this process for position 2, 3, 4, and so on. The first username in the dvwa.users table is admin. Try extracting the second character to verify.
- Test the second character against 'd'. Type this payload and click Submit:
1' AND SUBSTRING((SELECT user FROM dvwa.users LIMIT 0,1),2,1)='d'-- -
You should see "User ID exists in the database." The second character is 'd', confirming the username starts with "ad".
What does LIMIT 0,1 do?
LIMIT 0,1 tells the subquery to return only the first row (starting at offset 0, returning 1 row). To extract the second username, you would change it to LIMIT 1,1. This is how you iterate through all rows in the table.
Getting unexpected results?
Make sure your subquery is wrapped in parentheses. The SUBSTRING function needs the subquery result as a single value, which the parentheses and LIMIT 0,1 ensure.
If every payload returns "exists," double-check that you have the single quote before AND. Without it, you are not breaking out of the original query string.
Need more help? My blind SQL injection binary search payloads are not working correctly in DVWA.
Secret mission
Bypass Medium Security Filters
DVWA's Medium security level removes the text input and replaces it with a dropdown menu. You can't type payloads directly anymore. Can you find a way to inject SQL anyway?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine using Docker containers. There are no cloud services or ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers (dvwa and db services).
- Docker volume (dvwa) storing the MariaDB database.
- Docker images (ghcr.io/digininja/dvwa:latest and mariadb:10).
- Local project folder containing docker-compose.yml.
Keep everything running
No action needed. Choose this if you're still actively practising SQL injection techniques or plan to explore other DVWA modules.
- Your containers will restart automatically whenever Docker Desktop is running (thanks to the unless-stopped restart policy).
- Access DVWA any time at http://localhost:4280.
Pause - I'll come back to this later
Stop the running containers to free up memory and CPU, but keep your files and database volume intact so you can resume later.
- Open your terminal and navigate to the project folder containing docker-compose.yml.
- Stop and remove the containers by running:
docker compose down
- This stops both containers and removes the network, but keeps your dvwa volume and images so you can start back up quickly.
- When you're ready to resume, run docker compose up -d from the same folder.
Delete - I don't want to use this again
Remove all project resources completely. This frees up disk space and leaves no trace of the lab on your machine.
- Open your terminal and navigate to the project folder containing docker-compose.yml.
- Stop containers, remove networks, and delete the database volume by running:
docker compose down -v
- Remove the Docker images by running:
docker image rm ghcr.io/digininja/dvwa:latest mariadb:10
- Delete the local project folder. On macOS or Linux, run:
rm -rf ~/Desktop/dvwa-lab
- On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\dvwa-lab
- Confirm the images are gone by running:
docker images
- You should no longer see ghcr.io/digininja/dvwa or mariadb in the output.
Nice Work!
Nice Work!
You just compromised a web application using the same SQL injection techniques that real penetration testers and CTF players use every day.
You've learned how to:
- Set up a reusable Docker-based hacking lab with DVWA and extracted data from a vulnerable application using progressively advanced techniques.
- Extracted full database contents (usernames and password hashes) using UNION-based SQL injection and information_schema enumeration.
- Mastered blind boolean-based injection to extract data character-by-character without any visible output from the application.
- Secret Mission: Bypassed client-side security filters using HTTP request manipulation in browser Developer Tools.
Ready to quiz yourself?