背景: 在 sqli-labs 第五关时, 即使 sql 语句构造成功页面也没有回显出我们需要的信息, 看到了有使用双查询操作造成报错的方式获得数据库信息, 于是研究了一下双查询的报错原理, 总结了探索的过程, 整理出此文希望可以帮到感兴趣的人.
sqli-labs 闯关游戏下载地址: https://github.com/Audi-1/sqli-labs
双查询报错注入
需用到四个函数和一个 group by 语句:
group by ... --->分组语句 // 将查询的结果分类汇总
rand() --->随机数生成函数
floor() --->取整函数 // 用来对生成的随机数取整
concat() --->连接字符串
count() --->统计函数 // 结合 group by 语句统计分组后的数据
用 sqli-labs 中的数据库为示例, 首先先了解一下子查询的概念.
子查询又称为内部查询, 子查询允许把一个查询嵌套在另一个查询当中
简单的来说就是一个 select 中又嵌套了一个 select, 嵌套的这个 select 语句就是一个子查询.
连接数据库后用子查询测试一下:
MySQL> SELECT concat("test:",(SELECT database()))as a;
执行查询操作时, 子查询先开始, 所以 SELECT database()先执行, 然后查询到当前数据库名称 "security", 并将其传给 concat 函数, concat 函数在对字符进行连接, 于是显示出图上的结果.
然后是 rand()函数, 其作用是生成一个大于 0 小于 1 的随机浮点数, 如下:
floor()函数的作用是对传入的参数取整, 这里将 rand()生成的随机数做处理进行取整, 由于 rand()生成的值取整结果只能为 0, 所以我们这里做一点处理, 使其生成一个大于 0 小于 2 的随机值, 并对其取整:
MySQL> SELECT floor(rand()*2);
结果要么为 0 要么为 1
接下来结合子查询, 显示出数据库信息:
MySQL> SELECT concat((SELECT database()), floor(rand()*2)) from users;
由于 users 表中只有 13 条数据, 所以这里返回了 13 条数据
在注入中, 我们不知道库名表名, 往往借助 information_schema 这个库进行猜解
其中 information_schema.schemata 中包含了 MySQL 的所有库名, information_schema.tables 中包含了所有的表名, information_schema.columns 中包含了所有的列名.
示例如下:(我电脑中有 7 个数据库, 所以返回了 7 条数据)
现在加上 group by 语句对返回的数据进行分组处理
MySQL> SELECT concat((SELECT database()), floor(rand()*2))as a from information_schema.schemata group by a;
from 前的 as a, 是为 concat((SELECT database()), floor(rand()*2))这一串取了个别名, 后面使用 group by 分组时就不用打那么长一串了, 直接使用别名就像.
到这里都是基础知识的铺垫, 而且前面所有的查询操作都是返回库名和 "0,1" 的拼接结果, 然而在 sqli-labs 第五关这样网页无回显的环境下, 我们是看不到任何的信息的, 所以接下来才是正题, 我们要利用 count 函数和上面的操作构成 MySQL 内部错误, 然后通过报错的提示获得我们想要的信息.
(上面的 database()函数在实际注入中也可以换成其他的, 如 version(), 具体看你想要通过报错获得的信息)
这里增加一个聚合函数 count, 构造的语句如下:
MySQL> SELECT count(*),concat((SELECT database()), floor(rand()*2))as a from information_schema.schemata group by a;
这里利用 count(*)对前面的返回数据进行统计, 由于 group by 和随机数的原因, 有可能会出现重复的键值, 当键值重复时就会触发错误, 然后报错, 由于子查询在错误发生之前就已经完成, 所以子查询的内容会随着报错信息一起显示出来:
这里我使用的是 information_schema 中的 schemata 表, 因为我的数据库有 7 个, 生成的随机结果中 0 和 1 有一定比例, 不容易全是 0 或者全是 1, 实际使用情况下推荐使用 information_schema 中的 tables 或者 columns 两个表, 里面的数据条目较多, 容易生成较多的随机值.
例如:
MySQL> SELECT count(*), concat((SELECT database()), floor(rand()*2))as a from information_schema.tables group by a;
在 sqli-labs 闯关的第五关中 payload 如下:
XXX.PHP/?id=-1' union select 1,count(*), concat((select database()), floor(rand()*2))as a from information_schema.tables group by a --+
注意, 由于有随机性, 可能成功执行了语句所以不会报错, 正常的显示页面 (即不报错) 如下:
这种情况多提交几次就行, 理论上每次都有百分之 50 的可能性
但可以通过修改 rand()使用的种子来使其百分百报错, 如下将 rand()改为 rand(1), 测试百分之百报错:
XXX.PHP/?id=-1' union select 1,count(*), concat((select database()), floor(rand(1)*2))as a from information_schema.tables group by a --+
注入原理
以下是学习过程中看到的不同作者对该问题原因的解释:
这个是最初看到的原理, 但是个人觉得阐述的不太正确:
当在一个聚合函数后面 (比如 count) 后面使用分组语句, 就会把查询的一部分以错误形式显示出来; 因为 concat 函数执行两次, 比如 select database(), 这样就执行了两次 select database, 与后面的随机函数链接在一起, 可能会随机重复, 就会报错;
另一个博客中提出的深层次的原因, 比较合理:
通过 floor 报错的方法来爆数据的本质是 group by 语句的报错. group by 语句报错的原因是 floor(random(0)*2)的不确定性, 即可能为 0 也可能为 1,(group by key 的原理是循环读取数据的每一行, 将结果保存于临时表中. 读取每一行的 key 时, 如果 key 存在于临时表中, 则不在临时表中更新临时表中的数据; 如果该 key 不存在于临时表中, 则在临时表中插入 key 所在行的数据. group by floor(random(0)*2)出错的原因是 key 是个随机数, 检测临时表中 key 是否存在时计算了一下 floor(random(0)*2)可能为 0, 如果此时临时表只有 key 为 1 的行和不存在 key 为 0 的行, 那么数据库要将该条记录插入临时表, 由于是随机数, 插时又要计算一下随机值, 此时 floor(random(0)*2)结果可能为 1, 就会导致插入时冲突而报错. 即检测时和插入时两次计算了随机数的值.
结论是: 当与临时表里面的值进行比较, 如果不同, 就插入, 但是插入的时候又计算了一次, 所以如果插入时计算的值与直接比较的值不一样, 则报错!
但是上述两个理由我看了感觉还是有一些地方不明白, 感觉没有说到地方, 所以又自己探索了一番, 这一篇文章篇幅已经很长了, 所以留在下一篇里单独探讨吧.
本文参考链接:
初步了解双查询注入:
https://www.2cto.com/article/201303/192718.html
深入理解:
https://www.cnblogs.com/BloodZero/p/4660971.html
rand()的随机数种子的影响:
https://segmentfault.com/q/1010000000609508
来源: https://www.cnblogs.com/laoxiajiadeyun/p/10278512.html