跳转至

SQL 注入

漏洞扫描器之 SQL 注入检测, 这是一个系列,扫描器的每个基础扫描我都准备写一篇文章,以此督促自己。

SQl 注入

提到 SQL 注入,那就离不开一个神器 sqlmap。该漏洞模块检测打算从 sqlmap 中抽离出一部分SQL注入检测的部分,使用 go 实现一遍,sqlmap 源码太复杂,所以这里只是一部分,自己搞工程量太大了。

SQLMAP部分源码阅读

sqlmap 首先会对目标进行连接性检测,然后发送可能被拦截的 payload 进行 waf 检测(这一块可以作为整体扫描器的一个入口检测)。然后就是我们要关注的核心功能,SQL 注入的检测。

页面相似度

waf 检测中除了关键字匹配外,还会进行页面相似度算法进行匹配,并且会区分页面是否为动态的(第一次和第二次同样访问页面相似度小于 0.98),也就是每次访问页面的某些元素是否会变,比如广告。

当 sqlmap 认为这个页面是一个动态页面时,会尝试寻找该页面的动态内容(findDynamicContent),找到后默认会将动态内容的前 20 字符和后 20 字符做一个标记,方便后面对比判断时将动态内容移除。

  • 相似度比较有第三方库可供我们使用strsim
  • 标记动态内容,目前是粗暴的对比页面内容,将不同点提取出来,然后标记

这里我使用了diffmatchpatch 库(类似 git diff 的一种实现)来对比不同内容,然后简单粗暴的获取了第一块不同的点(可能有多个),再对前后 20 个字符进行标记,然后进行正则匹配进行内容替换,目前来看效果是和 sqlmap 一致,但没有经过大量测试,有待优化。

每次进行相似度检查前,先将动态部分替换,然后和模板页面(原始页面替换后的页面)进行对比

这一块搞好了,动态参数也就很容易了

前期准备这一块的实现还算简单,难点在于下面的检测方面。

注入检测

sqlmap 主要有两种检测(lib/controller/controller.py)

  • 启发式检测,对目标参数 sql 注入做一个初步的判断(第一个红框)
  • 注入检测(第二个红框)

image-20230207205442610

启发式检测

启发式注入的目的就是让Web应用报错,如果Web应用开启了错误回显,就可以快速识别DBMS。这个过程就是寻找闭合 sql 语句的一个行为,sqlmap 会根据参数类型采取不同的闭合方式,比如数字采用无闭合方式,字符串采用常见的单引号闭合

Python
# Alphabet used for heuristic checks
HEURISTIC_CHECK_ALPHABET = ('"', '\'', ')', '(', ',', '.')

payload 是由以上 6 个字符随机组合,并且同时满足'"都只有一个, 然后发送 payload, 看是否有报错说转型错误,如果有则代表该参数无法进行注入,后续就不在检测该参数。

启发式检查完后,又顺手执行 XSS 和 FI 检测

  • 检测 XSS 通过检查 "<'\">"加上随机字符,是否出现在了结果中
  • 检测 FI(文件包含)通过正则检测(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}

如果有报错信息的话,通过errors.xml匹配一下,确认数据库,方便后期精确的发送对应的 payload。

这里我的代码检测逻辑是如果页面存在报错信息,即认为存在 sql 注入,后续验证交给更专业的 sqlmap 工具。

注入检测

布尔盲注

布尔盲注过程中大量使用响应相似度分析技术,针对每一个注入点循环发包时候,sqlmap第一步就是进行设置临界点。

在发送第一个逻辑假包之后,sqlmap就开始进行网页相似度计算,首先移除动态随机字符串之后得出逻辑假页面与原始页面网页相似度值,若不相似,该网页相似度值将会被置为本次测试向量过程中的临界点,并且sqlmap默认的容差为0.05,也就是说,当一个响应的网页相似度大于临界点+0.05时,sqlmap认为该响应与原始页面相似,否则,与原始页面不相似。

之后进行布尔注入检测:

  • 发送逻辑真,相似
  • 发送逻辑假,不相似

认为存在注入,然后进行误报检测,生成三个不同的数字(eg:25、83、53),将这些数字组成不同的逻辑反复测试

  • AND 25=25 相似
  • AND 25=83 不相似
  • AND 83=25 不相似
  • AND 53=53 相似
  • AND 83 53 不相似

误报检测完成,确认存在注入

报错注入

这里我直接复用启发式检测 payload 结果,然后读取 errors.xml 字典,进行匹配,根据页面是否存在数据库报错信息来确认。本次实现没有进一步的使用 payload 探测, 实际上应该对不同的 dbms 发送不同的 payload,然后正则匹配返回内容进一步确定的(data/xml/payloads/error_based.xml)

这里实现的话,感觉有点复杂,这里只确认是否存在数据库报错信息,剩下的交给专业的 sqlmap 验证

时间盲注

首先计算了访问正常页面的响应时间,计算公式为:五次样本内平均响应时间 + 7 * 样本标准差,这样就可以保证过滤掉 99.99% 的无延迟请求。

标准差和方差一样都是用于衡量样本的离散程度的量,那么为什么要有标准差呢?因为方差和样本的“量纲”不一样,换句话说不在一个层次。怎么理解这个层次呢,从公式看方差是样本与均值的差的平方和的平均,这里有一个平方运算,这是导致量纲不在同一个层次的原因。

比如两个集合 [0,8,12,20]和 [8,9,11,12],两个集合的均值都是10,两个集合的方差分别是:69.33和3.33;计算两者的标准差分别是:8.3和1.8。数字越大代表越离散,从数值上看方差和标准差的量纲差别就很明显了,而标准差更好的在量纲上与样本集合保持同步。这就是“标准”的意义了。

为什么要加上 7 倍的标准差?

因为正常情况下,响应时间的分布是符合正态分布的,而正态分布的 99.99% 的数据都在 7 倍的标准差之内,所以这里就是为了过滤掉 99.99% 的无延迟请求。

Copilot 告诉我的, 还挺好使,哈哈

代码实现

Go
timeRec 是一个记录五次访问时间的数组
mean(timeRec) + 7*std(timeRec)

// 期望,也就是平均响应时间
func mean(v []float64) float64 {
    var res float64 = 0
    var n = len(v)
    for i := 0; i < n; i++ {
        res += v[i]
    }
    return res / float64(n)
}

// 方差
func variance(v []float64) float64 {
    var res float64 = 0
    var m = mean(v)
    var n = len(v)
    for i := 0; i < n; i++ {
        res += (v[i] - m) * (v[i] - m)
    }
    return res / float64(n-1)
}

// 标准差
func std(v []float64) float64 {
    return math.Sqrt(variance(v))
}
UNION 注入

猜解列数

核心思想就是 利用与模版页面比较的内容相似度寻找最最不同的那一个请求

猜解输出点在列中位置

参考

Yakit 启发式SQL注入插件

http://wjlshare.com/archives/1733

https://mp.weixin.qq.com/s/G_0tXDo_BWo_niR2afg0WQ

https://zhuanlan.zhihu.com/p/44157153

https://zhuanlan.zhihu.com/p/45291193

https://paper.seebug.org/729/#_9


最后更新: 2023-03-06