Blind SQL injection

This carries on from the SQL injection article. The previous example was a slightly contorted result since I was using information that I knew about the database and the results returned were appearing in the page. I was just trying to introduce the concepts. Now here I’m introducing automated blind SQL injection. Source code for this article is avaiable in a zip file.

I created another really badly written web app for a simple blog-like site. There’s a MySQL database backend containing pages and each page’s content is displayed from a database query.

Screenshot from Blind SQL Injection article

Vulnerable web app in action. The id parameter is not dealt with properly and through it we can read just about anything from the database.

The server takes a url parameter “id” to determine which page to display:

http://server/insecurewebapp.php?id=1

Unfortunately it doesn’t deal with this value correctly before using it in a database query and so we can use this to read data from the server, like this (note that these are not database queries running on the server, this is an attack program using only HTTP requests with craftily formed URLs):

screenshot of blind SQLi program in action, reading data from the database using cleverly crafted HTTP requests only

screenshot of blind SQLi program in action, reading data from the database using cleverly crafted HTTP requests only

2 things are important:
1. The line of code which grabs the id from the url (which is then used directly in the SQL query without encoding) does not convert it to an integer:


$id = $_GET['id'];

It should read:


$id = intval($_GET['id']);

2. If no page is found then a message “Page not found” is displayed. This is what we’ll use to return information from the server.

Note that fixing that one line of code there is all that it would take to fix the whole problem!

Together this means we can include our own SQL and set the “id” parameter to a query which resolves to 2 values which are passed to “id”. If these 2 values return different content then we can ask yes/no questions.

Now if we give an incorrect value (say “-1″ or “99999″ or even in this example “4″) then we get that “Page not found” message.

Note: you could use any 2 different returned pages and check for content unique to one of them. You could send usernames to a login screen using one username which exists and another that doesn’t and read a “user does not exist” message to return data. Or you could use 2 terms which return different results from a vulnerable search engine.

This is enough to read all sorts of data back from the server: the username that the web app uses to talk the database, the names of all the tables they can access, the contents of tables. OK, but how?

The blind SQL program works by phrasing the request so that it can turn a query into a series of yes/no requests and returns either a page or the “Page not found” message. It makes a select statement, say:
SELECT * FROM mytable;
and it choses one row at a time from the server using the LIMIT:
SELECT * FROM mytable LIMIT 0,1; — gives the first row
SELECT * FROM mytable LIMIT 1,1; — gives the second row, etc

So it uses:
SELECT username AS ts FROM users LIMIT 0,1; — “AS ts” renames the column as “ts” so it can be refered to elsewhere in the query

Then we check the letters one at a time and one character at a time. These 2 lines checks if the first letter of the first entry in the table “users” is “a” or “b”:
SELECT IF(MID(ts,1,1)=’a',1,-1) FROM (SELECT username AS ts FROM users LIMIT 0,1) AS t;
SELECT IF(MID(ts,1,1)=’b',1,-1) FROM (SELECT username AS ts FROM users LIMIT 0,1) AS t;

It returns a 1 if it is and a -1 if it isn’t.

These 2 lines checks if the second letter of the first entry in the table “users” is “a” or “b”:
SELECT IF(MID(ts,2,1)=’a',1,-1) FROM (SELECT username AS ts FROM users LIMIT 0,1) AS t;
SELECT IF(MID(ts,2,1)=’b',1,-1) FROM (SELECT username AS ts FROM users LIMIT 0,1) AS t;

And so on until you’ve finished the first entry.

These 2 lines checks if the first letter of the second entry in the table “users” is “a” or “b”:
SELECT IF(MID(ts,1,1)=’a',1,-1) FROM (SELECT username AS ts FROM users LIMIT 1,1) AS t;
SELECT IF(MID(ts,1,1)=’b',1,-1) FROM (SELECT username AS ts FROM users LIMIT 1,1) AS t;

The resulting yes/no answer is determined by checking the page for the phrase “Page not found”.

Now all we have to do is bombard the server with a few thousand requests (to see this do a tail -f on your server log while you’re testing) and we can query almost anything. The program reads the username the webapp is using to talk to the database, the name of the database (remember a server runs multiple database), pulls out a list of the tables for that database, pulls out the column names and contents for each table.

You can download the source code in a zip file.

To run the webapp I had to set up a database and some tables with some pretend web pages or you can un-comment the block of code at the start to run it against the code from my first SQL injection article.


CREATE DATABASE blindsqliwebapp;
USE blindsqliwebapp;
CREATE TABLE users (username CHAR(100), password CHAR(100));
INSERT INTO users VALUES ('admin', 'secret');
INSERT INTO users VALUES ('bob', 'haveaniceday');
INSERT INTO users VALUES ('Tim', 'HaHaha');
CREATE TABLE pages(id INT, page_content LONGTEXT, author CHAR(100));
INSERT INTO pages VALUES(1, 'A really intertesting first page.', 'bob');
INSERT INTO pages VALUES(2, 'The second page is just as exiting.', 'bob');
INSERT INTO pages VALUES(3, 'The third page just leads you wanting more.', 'bob');
GRANT ALL PRIVILEGES ON blindsqliwebapp.* TO "insecureuser"@"localhost" IDENTIFIED BY "mmmdonuts";
FLUSH PRIVILEGES;

Update:
I improved performance massively (it now needs less than half the number of requests) by checking for characters using a technique similar to binary search. For each character position in a value I’m trying to get from a row, I take all the characters and split them into 2 groups and check the first group. If it’s in the first group then I continue to use that and discard the second group otherwise I use the second group. I repeat this until I am down to one character. This makes the URLs much longer so might fall foul of filters but means I only need to do about 6 requests on average for each character.

Update:
I’ve used the letter frequency table from wikipedia to improve efficeincy even further. I work through the whole set of characters in groups of 8 first (since there’s a very high probablility of the chracter being in the top 8 occurring letters) and then I use the binary method on the smaller group. This has improved efficiency to about 4.9 requests per character that I figure out. Since I am testing for one of 66 characters then using a binary search alone would theoretically take log2(66) or 6.04439412 requests.

Share this:
Share this page via Email Share this page via Stumble Upon Share this page via Digg this Share this page via Facebook Share this page via Twitter

Comments

  1. Planet Larg will be hosting this as soon as I can sort it

Leave a Comment