-
启动流程
看一下去掉一般不会遇见的 else 和 except 后的 main 函数大致流程
try: dirtyPatches() # 修改环境变量,确保 sqlmap 在不同环境下都能正常使用 resolveCrossReferences() # 修改原函数的指针 checkEnvironment() # 检查环境(sqlmap 存放路径,版本,全局变量初始化) setPaths(modulePath()) # 把 sqlmap 的路径信息存到变量 path 中 banner() # 输出 sqlmao 启动时那个图形。。。 args = cmdLineParser() # 解析传入的参数,部分参数赋值,解析后把所有的参数合并到 cmdLineOptions 中继续使用 cmdLineOptions.update(args.__dict__ if hasattr(args, "__dict__") else args) initOptions(cmdLineOptions) # 对几个全局变量进行初始化的操作 init() # 对命令行中的部分参数进行处理,并为全局变量的部分参数赋实际值 start()
进入 start(),对参数进行特殊处理后会遍历 kb.targets 中的数据,队每一组数据进行注入测试
然后连续调用两个函数分别初始化当前 target 的基本环境,并对传入的 url 进行解析
initTargetEnv() parseTargetUrl()
然后检查 url 中是否存在 data(get请求),用于确定 paramKey 的值
这里有一步会判断当前扫描的 url 是否在已确认存在漏洞的列表中
然后执行 setupTargetEnv 进一步设置环境,这里已经解析完参数了,再进到 _setRequestParams 中把从 GET/POST 请求中的参数转化为字典后封装到 conf.paramDict 与 conf.parameters中
然后发起首次链接,并以此次请求的 response 作为原始信息与后续的 response 做比较
接着进入 checkwaf,比较有趣的是这里参考了 nmap 的一个 checkwaf 的 payload,最后是生成了一个含大量危险特征值的字符串
最后的一长串,checkwaf 的后半段会比较这个请求的 response 与初始值的相似度(<0.5)是认为有 waf
randomInt() + AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert(\"XSS\")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#"
当没有设置
--string
、--not-string
、--regexp
并且 BOOLEAN 在--technique
中时调用 checkStability 函数进行页面的动态性检测然后设置 http 请求头, PLACE 中存放的就是之前参数解析出来的参数,后续会根据不同的 level 修改请求头
接下来会根据 BOOLEAN 是否在
--technique
来判断是否需要调用 checkDynParam 函数进行参数的动态性检测,如果参数是静态的并设置了 --skip-static 就会跳过这个参数的检测。最后进行一个”启发性测试“,会通过搜集页面错误信息来在实际注入前尝试获取目标的部分信息以及测试其余漏洞是否有可能存在。
-
关于动态检测
sqlmap 即使不指定参数也能正常测试注入就依赖于这个函数(checkStability)。
首先会重新发一个请求,直接判断这次的 response 和 第一次的 response ,如果完全相同则跳过,不同则会询问下一步操作
一般这时就直接点 C 了,进入 checkDynamicContent 中,此函数首先会对 firstPage 与 secondPage 进行判空和判断是否超出了最大检测长度的前置步骤,随后会通过 difflib.SequenceMatcher 进行相似度比对(之后会在 while 循环中持续比对,直到相似度<0.98)
然后用 findDynamicContent 进行具体的动态部分的查找,比较的还是 fistPage 和 secondPage,这里是把很长的 response 分块比较,通过 get_matching_blocks 函数获取两个字符串中相同的部分实现分块,在后续操作中提出动态的部分
>>> list(s.get_matching_blocks()) [Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
随后 sqlmap 会将动态内容的前20个字符与动态内容的后20个字符存入 kb 中,后续在进行测试时会首先通过正则去除前缀与后缀之间的内容以确保后相似度检测的准确率。(但在 JSON 场景下,前缀和后缀可能<20)。
参数的动态检测靠 checkDynParam 函数实现的
创建了一个随机整数,在调用 queryPage 的时候换成参数值,然后对比 response 和原相应的相似度(0.98),进而判断这个参数是不是动态的。
这里的重点就是 queryPage 函数,以及它的几个相关调用
- ##### queryPage
如果有 tamper,就用其修改 payload
发请求部分
在 comparison 中进行相似度对比
- ##### comparison
具体代码不贴了,大致流程就是先处理
--string
、--not-string
、--regexp
这些设置,再通过 removeDynamicContent 去除动态值,然后经过编码处理确保编码一致性,最后计算相似度。关于相似度的判断:当相似度大于0.98时认为页面与原页面相似 当相似度小于0.02时认为页面与原页面不相似 当相似度与临界值之差大于0.05时认为页面与原页面相似
- ##### 启发性测试
- 生成一个包含特殊字符的 payload
\"().
,并发送(看看会不会报错 - wasLastResponseDBMSError 判断是否有与数据库有关的 ERROR(有的话就可能存在 sql)
- 判断
FORMAT_EXCEPTION_STRINGS
中的字符串是否包含在响应中(检测是否存在类型转换错误) - 生成一个包含
'"<>'
的 payload并发送(测 xss) - 通过
FI_ERROR_REGEX
这个正则匹配页面,如果匹配成功则认为可能存在 FI 相关的漏洞。
上述步骤可以看作一个最简单的漏扫...
-
注入部分
看一下最核心的 checkSqlInjection 函数,处理流程:
- 根据已知参数类型筛选 boundary
-
启发式检测数据库类型 heuristicCheckDbms(如果之前没有检测出或者传入)
-
payload 预处理(UNION)
-
过滤与排除不合适的测试用例
-
对筛选出的边界进行遍历与 payload 整合
-
payload 渲染
-
针对四种类型的注入分别进行 response 的响应和处理
-
得出结果,返回结果
- ##### payload部分:
sqlmap 的 payload 构成, 其中 prefix、comment、suffix 都是闭合注入点的前后部分, test 是最后执行的语句.
<prefix> <test> <comment> <suffix>
其中 prefix 和 suffix 是通用的,与数据库类型和注入类型无关的,因此单独作为boundaries 保存。
而 test 和 comment 需要具体分类,以 xml 的格式放在 payloads 文件夹下
test 中标签的具体含义:
<stype> 表示注入的类型。 <level> 测试的级别 <risk> 对目标数据库的损坏程度 <clause> 表明 <test> 对应的测试 Payload 适用于哪种类型的 SQL 语句 <where> 放具体注入语句的位置 <vector> payload大致什么样,并不是实际请求中的 payload <request> payload 的配置 <payload> 实际测试使用的 Payload <comment> payload 中的 comment 部分 <char> 只有 UNION 注入存在的字段 <columns> 只有 UNION 注入存在的字段 <response> 处理请求的方式 <comparison> 针对布尔盲注的特有字段,表示对比和 request 中请求的结果。 <grep> 针对报错型注入的特有字段,使用正则表达式去匹配结果。 <time> 针对时间盲注 <union> 处理 UNION •注入的办法。 <details> 如果 response 标签中的检测结果成功了,可以推断出什么结论? <dbms> 数据库类型 <dbms_version> 数据库版本 <os> 系统版本
- ##### 数据库类型检测
其实数据库类型是在之前确定的,来源于用户设定或者自动检测,但如果没确定就通过 heuristicCheckDbms 实现。核心原理就是利用简单的布尔盲注构造一个 (SELECT “[RANDSTR]” [FROM_DUMMY_TABLE.get(dbms)] )=”[RANDSTR1]” 和 (SELECT ‘[RANDSTR]’ [FROM_DUMMY_TABLE.get(dbms)] )='[RANDSTR1]’ 这两个 Payload 的请求判断。其中
FROM_DUMMY_TABLE = { DBMS.ORACLE: " FROM DUAL", DBMS.ACCESS: " FROM MSysAccessObjects", DBMS.FIREBIRD: " FROM RDB$DATABASE", DBMS.MAXDB: " FROM VERSIONS", DBMS.DB2: " FROM SYSIBM.SYSDUMMY1", DBMS.HSQLDB: " FROM INFORMATION_SCHEMA.SYSTEM_USERS", DBMS.INFORMIX: " FROM SYSMASTER:SYSDUAL" }
原理见后续布尔盲注检测。
其实如果检测不出来还是可以硬跑的。。。
- ##### 针对 boolean 盲注的检测
随便找一个 payload:
<test> <title>PostgreSQL OR boolean-based blind - WHERE or HAVING clause (CAST)</title> <stype>1</stype> <level>3</level> <risk>3</risk> <clause>1</clause> <where>2</where> <vector>OR (SELECT (CASE WHEN ([INFERENCE]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</vector> <request> <payload>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</payload> </request> <response> <comparison>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</comparison> </response> <details> <dbms>PostgreSQL</dbms> </details> </test>
发送了两个请求,并对 response 进行比对,其中第一次请求为正请求(Positive),对应 request.payload 的语句,第二次为负请求(Negative),对应 response.comparison 中的语句。
首先设置边界,比较负请求的 response 、原始页面和启发式页面之间是否存在差异,其中
kb.matchRatio
是原始页面和id=原始值
+"),'.)(((,
报错页面的相似度。发送正请求,结果必须是正请求与原始页面相同,与负请求不同,而且不能有 nullConnect 优化,就会进行负请求
如果负请求结果与原始页面不同,如果此时 negativeLogic(由where标签设置,说明参数错误的时候与模板页面不同才有意义),就会构造一个错误的 payload 并发送,并检查结果;如果启发式页面检查成功(elif语句),则会重新设置边界。
如果没有设置区分页面是否相同的选项,或相似度无法区分的时候,并不代表页面不相同,就会尝试提取长度 >10 的特征字符串作为特征,进而判断是否存在注入点。
- ##### 针对 GREP 型(报错注入)
test payload:
<test> <title>MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)</title> <stype>2</stype> <level>5</level> <risk>1</risk> <clause>1,2,3,9</clause> <where>1</where> <vector>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) USING utf8)))</vector> <request> <payload>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) USING utf8)))</payload> </request> <response> <grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep> </response> <details> <dbms>MySQL</dbms> <dbms_version>>= 5.7.8</dbms_version> </details> </test>
sqlmap 会用正则匹配 response 的内容,如果成功提取了对应内容,就说明可以进行注入,这里的 response 内容包括
- 页面内容
- HTTP 错误页面
- Headers 中的内容
- 重定向信息
-
针对 TIME 型注入
思路就是检查响应延迟的实验是否符合 payload设定的延迟,但重点是如何设置具体的延迟时间。
结论:正常请求 99% 的概率响应时间 <= 平均响应时间+7*标准差
最后有一个 mysql 的 patch(MySQL’s SLEEP(X) lasts 0.05 seconds shorter on average)
delta = threadData.lastQueryDuration - conf.timeSec if Backend.getIdentifiedDbms() in (DBMS.MYSQL,): # MySQL's SLEEP(X) lasts 0.05 seconds shorter on average delta += 0.05 return delta >= 0
- ##### UNION 型注入
核心逻辑在 uniontest 中的 _unionTestByCharBruteforce,其实在这个函数中就是实现了常规的 union 注入,要解决的难点就是列数的判断和输出点的确认。
- 使用 ORDER BY 查询,直接通过与模版页面的比较来获取列数。(二分法)
- 当 ORDER BY 失效的时候,使用多次 UNION SELECT 不同列数,获取多个 Ratio,通过区分 Ratio 来区分哪一个是正确的列数。
如果成功找到了列数,下一步就是确认输出点,我们只需要将 UNION SELECT NULL, NULL, NULL, NULL, … 中的各种 NULL 依次替换,然后在结果中寻找被我们插入的随机的字符串,就可以定位到输出点的位置。
调用 _unionPosition 进行查找
-
参考文献
- https://www.anquanke.com/post/id/262847
- https://www.anquanke.com/post/id/160636#h3-2
- https://www.anquanke.com/post/id/167408