0x00 前言
SQLMAP是一款测试SQL注入的神器,在大概一年前就有了想法,但是那时候静态看的一脸懵逼再加上个人的懒惰所以导致我没有继续看下去。
那么为什么我又会来看呢?主要有几个原因
- 想知道 SQLMap 高效测试 SQL 注入的一个逻辑
- 学习 SQLMap 的工程结构,学习其中处理的比较好的点
- 学习 SQLMap 中的一些我个人比较感兴趣的功能点:启发式扫描、页面相似度、时间盲注的检测、结果缓存的处理等
- 最近想做一些自动化 Fuzz 的东西并深知自己代码能力的薄弱,所以在做之前先看一下 SQLMAP 源码学习一下
但是光看不做点东西总感觉和没看一样,正好最近在学习 Go,所以也尝试用 Go 来仿制一个 SQLMAP ,不求很好用只求弄明白 SQLMAP 中的一些核心功能并提炼出来以此在别的场景能用到
本文只会针对一些我个人比较感兴趣的功能点进行介绍(毕竟sqlmap本身也比较复杂),所以不可能面面俱到,还请海涵
0x01 前期准备
SQLMAP 如果不结合动态调试的话是会很头大的,尤其是其中的 kb、conf 这两个值
靶场准备:SQLi-labs - Docker
docker pull acgpiano/sqli-labs
docker run -dt --name sqli-lab -p [PORT]:80 acgpiano/sqli-labs:latest
Pycharm:直接debug 配置界面添加参数就可以了
0x02 正文
在实际阅读源码并且通过 go 仿写的过程中发现 sqlmap 远比我想象中要复杂,所以我先从最简单的报错注入检测到爆库进行了阅读,先大致过一下sqlmap的一个整体流程
https://github.com/KpLi0rn/Gosqlmap-Beta
0x03 初始化
在还没解析 URL 发请求之前 SQLMAP 首先会对一些环境、变量来做一些初始化的处理
initOptions
: 解析我们的命令行参数
init
: 初始化函数
在 init 函数中通过调用各种函数进行参数的设置、payload 的加载等
我个人比较关注的是 payload 加载的部分,我红框框出来的这几个函数就是 sqlmap 加载 payload 的主要函数
loadBoundaries() // 加载闭合符集合
loadPayloads() // 加载payload集合
_loadQueries() // 加载查询语句,在检测到注入点之后后续进行数据库库名字段名爆破会用到的语句
挑选一个函数来看,发现逻辑就是从 data 下的 xml 中加载对应的 xml 文件,调用 parseXmlNode 函数进行解析
最终添加到 conf 对象的 tests 属性里
动态调试一下可以很清晰的看到各种 payload
在 sqlmap 中可以经常看到 conf 和 kb 这两个变量
conf 属性中主要存储了一些目标的相关信息(hostname、path、请求参数等等)以及一些配置信息 (init加载的payload、请求头header、cookie等),kb属性的话遇到再说
xml 中自然就是一些 payload 相关的信息
init 函数执行完毕之后,就会来到 start 函数进行项目的正式运行
如果觉得 sqlmap 代码太多也可以参考我go写的简化版的代码,init 地址为:https://github.com/KpLi0rn/Gosqlmap-Beta/blob/main/lib/core/option.go
0x04 URL
在初始化之后进行 start 函数之后,会先对我们输入的 URL 进行解析,即调用 parseTargetUrl 函数
简单看一下 sqlmap 中是如何处理我们输入的 url 的
首先如果我们传入的 url 并不是协议开头,正则没有匹配到的情况下就会先进行端口匹配如果发现 :443 就判定为 https 反之为 http
然后对传入 url 进行冒号分离,分别获取协议、路径、hostname这些
然后将各部分组合起来赋给 url
解析完URL之后会先对目标进行一次链接检测,如果发现无法连接就返回 False
然后就是 waf 检测
0x05 WAF检测
代码还是动态看比较容易看懂,正好我的博客上了阿里云waf,所以就来看一下 sqlmap 是如何做检测的吧
首先 sqlmap 生成了百分百会被 waf 拦截的 payload 例如 :<script>alert(1)</script>
这种
然后将payload作为参数传入 queryPage 来发起请求
在 getPage 中会调用 processResponse 来处理响应结果
在函数中可以看到会对response做一个检查
跟进发现通过正则表达式来对页面进行一个匹配
正则对应的规则在 thirdparty/identywaf/data.json 中,可以看到有正则和签名来进行判断
同时 sqlmap 不光通过规则库来进行判断,也会通过页面相似度来判断是否存在 waf/ips
如果相似度小于设定的 0.5 那么就判定为有 waf 拦截
具体关于相似度的代码分析我打算另开一篇来好好看看放在本文的话篇幅就有点过长了
0x06 注入检测
waf 检测结束之后就开始进入到核心部分,SQL 注入的检测了
sqlmap 这里主要有两个检测,第一个红框处是启发式检测,对目标参数 sql 注入做一个初步的判断,第二个是注入检测
启发式检测
首先会随机生成会造成sql闭合错误的payload
并发送请求,通过是否报错来进行快速判断
注入检测
报错注入检测
启发式检测是初步判断,那么现在就是正式开始注入检测了
首先是将 pyload 全部加载到 tests 变量中,然后 while 进行遍历
首先会根据前面的启发式检测的结果来初步确定数据库
然后就是从 tests 中弹出的 payload 进入 cleanupPayload函数对payload 进行清理
在 cleanupPayload 函数中对针对 payload 中的各种标签进行替换
替换后大致张这样
AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('qpbvq',(SELECT (ELT(5688=5688,1))),'qpkqq','x'))s), 8446744073709551610, 8446744073709551610)))
在 sqlmap 中将payload 分为了三部分,上面生成的 fstpayload 就是中间那部分
prefix + payload + suffix
prefix 和 suffix 就是对应的,闭合前面的结合以及注释后面的结构,这两个属性主要是从 boundary 中进行获取的,boundary 就是前面加载的 boundaries.xml 配置文件
并分别对 prefix 和 suffix 进行 clean,然后进行组合,组合之后的payload就是 reqPayload,然后进行请求
通过 queryPage 请求页面,然后对目标页面进行正则提取,页面结果是为 kb.chars.start 和 kb.chars.stop 包裹着的
这里 kb.chars.start -> qpbvq kb.chars.stop-> qpkqq
所以去掉包裹的话如果发现为 1 就说明注入存在,并且设置 injectable 为 True
0x07 爆数据库
在 checkSqlInjection 函数中判断了sql注入是否存在,如果存在的话就会输出 payload 以及对应的信息,这里输出的 Title 和 Type 都是在 xml 中 payload 中对应着的
输出信息之后就是要对数据库名等后续信息进行获取,主要是在 action 函数中,我们这里只介绍如何获取 dbs 所以这里就来看爆库这块
核心函数是 getDbs
来到 getDbs ,首先是判断是否存在缓存,如果存在缓存就直接返回缓存的结果,如果没有就从 queries 中获取payload语句
queries 就是存放之前初始化 queries.xml 的变量
sqlmap 爆库的逻辑也和常规的一样,首先通过 count(schema_name) 来获取数据库的个数,然后再通过 limit num,1 来依次获取数据库名
从queries 变量中获取语句之后就会传递到 getValue 函数
在 getValue 函数中前面主要是一些基础设置和一些对payload 的处理这里我们不用去关注,直接来看关键位置 errorUse
在 errorUse 中首先通过正则将 payload 中的各个部分都进行了获取 ,这里指返回了 payload 中的 schema_name
对 schema_name 部分进行了处理,将 schema_name -> count(schema_name) 然后传递给了 _oneShotErrorUse 函数调用
在 _oneShotErrorUse 函数中主要是处理形成最终 payload 并发送请求通过正则获取结果
sqlmap 会把我们前面的 payload 放到 [QUERY] 中进行替换,然后通过 queryPage 发送请求
然后将结果传入 extractRegexResult 函数中进行正则提取
ps: 这里提一嘴,sqlmap 这里是采用的多线程
在上面 sqlmap 知道了数据库数量之后就会 while 依次调用获取数据库名了,另外 sqlmap 中各种类型的都在 lib/techniques 文件夹中
如果觉得sqlmap不清晰也可以看一下我用go写的 https://github.com/KpLi0rn/Gosqlmap-Beta/blob/main/lib/techniques/error/use.go 应该能帮助到您
0x08 总结
当然一篇文章是根本不可能把 sqlmap 介绍清楚的,所以本文只是 part 1,sqlmap 能做到如此高效的检测/利用在代码中其实有着很多细节处理,像一些装饰器、缓存、判断动态参数、误报等代码我都没有在文中涉及而这些也都是让sqlmap变的可靠/高效的一部分,本文主要是简单的介绍了 sqlmap 从初始化到报错注入利用的一个流程
木神最后一行字打错了,是高效打成搞笑了
这就改过来
师傅,断更了呀?