DVWA - SQL injection

Starting the challenge

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.

Low level

Understanding the application

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.

SQLI application

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.

SQL injection

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.

Exploiting the vulnerability

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.

SQLI low check

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 #.

SQLI low attack

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:

SQLI low success

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

Vulnerable code

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.

Remediation

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.

Medium level

Understanding the application

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.

SQLI application

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.

Exploiting the vulnerability

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#.

SQLI application

The server executed our request and selected and displayed every user.

Vulnerability

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.

Remediation

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.

High level

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

SQLI application

When we enter the string ' as the ID the page does not reload properly, we get a blank page with something went wrong written.

Finding the exploit

Double request

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.

Macro configuration

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 :

  1. Project Options > Sessions > Macros > Add
  2. Select the second request, the one calling GET /vulnerabilities/sqli/
  3. Click OK
  4. On the Macro Editor window, give a name to the macro and click OK.
  5. Project Options > Sessions > Sessions Handling Rules > Add
  6. In Rules Actions click Add > Run a post request macro.
  7. Select the newly created macro
  8. In Scope > Tools Scope select only Intruder and Repeater
  9. In Scope > URL Scope select Use suite scope
  10. In Details give a name to your rule and click OK
  11. Select your rule and click Up so it is the first one.

Because of the step 9. you should also set your target scope.

SQLI add macro

SQLI add session rule

SQLI session rule scope

Intruder configuration

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.

SQLI high attack

In our case the injection OR 1=1-- worked properly.

Vulnerability

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.

Remediation

The developer should use prepared statements to sanitize the user input before executing the SQL queries.

Impossible level

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();

?>