DVWA - SQL Injection

Testing the SQLi section of the DVWA

I have been improving my application security skills by enrolling in the free Web Application Security curriculum, made by researchers at PortSwigger. I highly recommend this training as being free of charge is not its only advantage: it has a diverse and well curated content, and multiple online labs to test the newly gained knowledge. Last but not least, it is presented as being the third edition to the reference book in the web application security field, The Web Application Hacker’s Handbook

To test my skills in a live app, I used the Damn Vulnerable Web App (DVWA), a vulnerable web app that has been around for many years, and is one of the go-to playground to test one’s ability to find and exploit vulnerabilities.

This lab is a writeup of the SQLi section of the DVWA app. There are four different difficulty levels, indicating how hard it is to break the app (or how securely a component is written). Low means no security measure at all, Impossible indicates that the app should be secure against all vulnerabilities, and is used to compare the vulnerable source code, to the secure one. There are also Medium and High in between.

For this lab I chose the High difficulty level. I have tried lower difficulty levels in the Command Injection section, and found it not challenging enough. That being said, I did not encounter any issue or block during my experiment with the High security level.

In the next sections I will provide my step by step walkthrough, then discuss the vulnerable code, and provide a few security recommendations.

Poking around before attacking

The web page vulnerable to SQLi displays a “Click here to change your ID” text with a link that, once clicked, opens a popup page with a text box allowing end users to submit input. Entering numbers, such as 1 in the example below, seem to give information about users from a database. There are 3 fields: ID, First Name, and Surname

ID = 3 is a user whose First Name is Hack, Surname is Me. Interesting. Providing consecutive ID numbers allows us to retrieve information about up to 5 users, including Hack Me and Admin. This does not necessarily mean that there are 5 users, as ID are not always implemented to be consecutive numbers. For now, we will assume that we have at least 5 user records.

Determining if the change ID form is vulnerable to SQLi

To determine if the form is vulnerable to SQLi, we simply enter ‘-- in the text form. Upon submitting, this returns an SQL error and indicates that the database is a MariaDB database. From this we infer that the syntax we will need to make comments is -- (with a whitespace at the end, for MariaDB products)

The error message divulged the database product. This indicates that some SQL errors or exceptions are not caught and handled at the app layer, and are returned as is to end users. We will discuss this further in the last section, security recomendations.

Getting User Credentials

Since the data we have provides information about users, we want to obtain user credentials, typically a (hashed) password if those are app users. To get more information, we will try to leverage UNION SELECT attacks. Union attacks potentially allow us to retrieve information from other tables when SQLi is possible.

We need to first determine the number of columns, and data type of the information that is returned to us when we submit legitimate input, so we will know how to formulate UNION SELECT attacks in later steps.

Determining the Number of Columns

To determine how many columns are returned in the response from the change ID form, we submit UNION SELECT NULL payloads, with various number of NULL, until we do not have an error. This informs us that there are 2 columns returned in the response.

Determining the Data Type

Now, let’s determine the columns data type. We are searching for username / (hashed) password data, that is most likely stored in string format. We need to have at least one column returning string data, so that we can leverage it when retrieving the data we want. One column is enough, as we can concatenate information from username and password into that column.

For this, we submit UNION SELECT payloads and place a string value in either the first, second or both payloads to see when we do not have an error. Luckily for us, both columns hold string data:

Extracting User Credentials

We now know that we can construct queries that return 2 string columns. Let’s collect more information about the database, and start with the list of tables:

1' UNION SELECT table_name, NULL from information_schema.tables--

This runs successfully, and indicates that there are 2 non system tables, Guestbook and Users. Let’s have a look at the Users Table.

1’ UNION SELECT column_name, NULL from information_schema.columns WHERE table_name='users’--

(Note that I also looked at the Guestbook table using a similar command, and did not retrieve interesting data.)

This is a very interesting table that contains 8 columns, including user and password. Alright, it’s time to go for what we were looking for:

1’ UNION SELECT user, password FROM users--

We now have the list of user names and hashed passwords. We can feed one of the hashes to an online hash decoder website.

The algorithm used was MD5. From there, we can break the hash using the online hash decoders (that most likely combines dictionary attacks and brute forcing). And Yesss!! We got admin’s and gordon’s passwords 🥳🎉!!

gordon: abc123

Security Recommendations

  • Let’s compare the source code between High and Impossible. The main difference we notice is that in the High level, we pass the user input directly in the SQL command syntax, while in the Impossible level, we use prepared statements as shown in the snippet below. We should always use prepared statements, as they allow user input to be separated from the actual SQL command. Being able use our custom input and add it in SQL statements performed on the database, is what made the app vulnerable to SQLi.
// 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 );
  • We should catch SQL errors at the app layer and provide:
    • Either no feedback as a prefered method (unless there is a business requirement to inform the user to provide a better suited input)
    • Or, if there is a need to inform end users an error occured, use custom error messages that only reveal what is needed.