SQLMAP源码分析
分析 Sqlmap 源码时的疑问
Sqlmap 的使用
常用的参数如下:
bash -v (-vvv)打印不同等级的log输出 -u (-u http://192.168.74.1/Less-1/?id=1)指定目标url -p (-p id),指定注入参数 --dbms (--dbms mysql),设置数据库类型,不用再探测指纹 --level (--level 5),不同的level允许的payloads目录下的使用的payload等级也都不一样 --risk (--risk 3),同上类似,高等级的risk存在大量的时间盲注和布尔盲注 [ --string | --not-string | --regexp | --code ] (--string str1)用于进行页面对比,常用于布尔盲注 --technique [B|E|U|S|T|Q] (--technique BEUSTQ)测试时使用的sql注入类型(B:布尔盲注| E:报错注入| U:union注入| S:堆叠查询注入| T:时间盲注| Q:内联查询) https://sqlmap.kvko.live/usage/techniques
2. sqlmap 各种攻击方式
1. 枚举
bash -all 获取远程获取所有可访问数据信息 --dbs 获取所有数据库名称 --tables 获取所有表 --columns 枚举列名[-C,-T,-D],使用-T指定表名,-D指定库名,-C指定列名(-columns -D testdb -T users -C name) --schema 获取所有schema信息 --count 获取导出所需表数据之前知道表中的条目数,(--count -D testdb) --dump-all 导出所有条目 --sql-query --sql-shell 执行自定义sql语句 -b 获取数据库banner信息 --current-user 获取当前用户名称 --current-db 获取当前数据库名称 --hostname 获取主机名 --is-dba 确认当前会话是否未数据库管理员(DBA) --users 枚举用户 --passwords 枚举所有用户密码的hash值 --privileges 枚举每个数据库用户权限 --roles 枚举出每个 DBMS 用户的角色
2. 暴力破解
主要存在如下情况才会用暴力破解 (--common-tables 和 --common-columns , 字典位于 txt\common-columns.txt 和 txt\common-tables.txt):
- < 5.0 版本的 MySQL,不具备 information_schema
- 是微软的 Access 数据库,并且其中的系统表 MSysObjects 默认设置不可读。
- 对于系统表没有读权限
3. 用户自定义函数注入
使用--udf-inject 和 --shared-lib
4. 访问文件系统
原理详见=> 通过高级SQL注入完全控制操作系统
- --file-read
- --file-write
- --file-dest
5. 接管操作系统
- 运行任意操作系统命令
- --os-cmd
- --os-shell
- 有状态带外连接
- --os-pwn
- --os-smbrelay
- --os-bof
- --priv-esc
- --msf-path
- --tmp-path
6. 访问 Windows 注册表
--reg-read, --reg-add, --reg-del,--reg-key,--reg-value,--reg-data 和 --reg-type
- 完整的 sqlmap 各参数如下: https://github.com/sqlmapproject/sqlmap/wiki/Usage
Sqlmap 文件目录
-
- sqlmap/plugins
- dbms->各个数据库注入的核心方法:判断 DBMS,进行注入等操作
- generic->通用文件夹
- sqlmap/data
- procs->存储进程访问(一些常用的 sql 语句)
- mssqlserver
- mysql
- oracle
- postgresql
-
- backdoor->webshell
- stagers->用于上传文件
-
- mysql
- postgresql
-
- common-columns->数据库中的常用列
- common-tables->数据库中的常用表
- common-files->常见的敏感文件目录
- common-outputs->数据库中常用的函数
- keywords->sql 中的 keywords
- smalldict->数据库中的字典
- user-agents->sqlmap 使用的 UA
- wordlist. tx_->字典压缩文件
- xml->sqlmap 使用的信息
- banner->主要用来识别各数据库指纹信息
- payloads->包含 sqlmap 使用时 6 种注入方式的不同 payload
- 布尔注入 (boolean_blind)
- 报错注入 (error_based)
- 内联查询 (inline_query)
- 堆查询 (stacked_queries)
- 延时注入 (time_blind)
- 联合查询(union)
- errors. xml->用于根据报错信息匹配数据库类型
- queries. xml->记录数据库查询语句类型
- boundaries. xml->记录的是 sql 注入时边界信息例如
‘(等信息
- procs->存储进程访问(一些常用的 sql 语句)
- sqlmap/libs
- controller 目录
- action. py->利用 URL 收到影响的参数进行 SQL 注入,并且在条件许可下抽取系统或者数据库中的数据
- checks. py->利用 payload 对 SQL 注入点进行注入检测
- controller. py->对用户传递的参数进行控制
- handler. py->对用户传递的数据库类型(mysql, orcale, mssql 等)进行设置不同的
handler
- core 目录->各种参数调用?
- parse 目录
- request 目录
- takeover 目录->接管目录
- technique 目录->不同的注入类型都有相应的文件夹
- utils 目录
- controller 目录
- sqlmap/plugins
- dmbs 目录->存放各种的数据库的 handler ,用于进行对应的 sql 注入
- generic 目录
- sqlmap/tamper->注入绕过防火墙的脚本 ✅ 2023-04-01
- sqlmap/thirdparty 第三方插件
Sqlmap 调试环境
- 用 phpstudy 搭建 sqli-labs 环境
- 使用 sqlmap 进行测试
- sqlmap 使用
-v设置输出等级时有 7 个级别:- 0:只输出 Python 出错回溯信息,错误和关键信息。
- 1:增加输出普通信息和警告信息。
- 2:增加输出调试信息。
- 3:增加输出已注入的 payloads。
- 4:增加输出 HTTP 请求。
- 5:增加输出 HTTP 响应头
- 6:增加输出 HTTP 响应内容。
用等级 2 能更好地了解 sqlmap 内部实现了什么,特别是在检测阶段和使用接管功能时。果你想知道 sqlmap 发送了什么 SQL payloads,等级 3 是最佳选择。同时还能够使用-t TRAFFICFILE选项保存对应的流量。
因此在调试时选择 v3 等级并且打印的方式->python3 .\sqlmap. py -u " http://192.168.74.1/Less-1/?id=1" -t ".\log\test. txt" -vvv
- 测试不同的 SQL 注入方式:
- 布尔注入 (boolean_blind)
- 报错注入 (error_based)
- 内联查询 (inline_query)
- 堆查询 (stacked_queries)
- 延时注入 (time_blind)
- 联合查询(union)
- sqlmap 使用
Sqlmap 的检测流程
1. 初始环境准备
主要代码如下:
def main():
"""
Main function of sqlmap when running from command line.
"""
try:
# dirtyPatches 修改python中的变量值
# 修改 HTTP Client 的最大长度为 1024 * 1024
# Patch HTTP Client 默认的 __send_output 方法
# 修改 HTTP Client 默认的 LineAndFileWrapper 方法
# 修改默认的 os.urandom方法 生成为输入长度的随机字符串
dirtyPatches()
# resolveCrossReferences 函数修改原函数的指针。
resolveCrossReferences()
# checkEnvironment 函数会对当前的环境进行初步检查,包括:
# sqlmap 存放的绝对路径中是否包含非 ascii 字符
# sqlmap 的版本是否小于1.0(新环境运行着旧脚本)
# 全局变量的初始化(当 sqlmap 被作为包导入时)
checkEnvironment()
# setPaths 函数会将 sqlmap 的路径信息存入 paths 这个全局变量中。
setPaths(modulePath())
banner()
# Store original command line options for possible later restoration
args = cmdLineParser()
# ...
# initOptions 函数会对几个全局变量进行初始化的操作(conf,kb,mergedOptions)。
initOptions(cmdLineOptions)
# ....
# 函数会对命令行中的部分参数进行处理,并为全局变量的部分参数赋实际值。
init()
# 各种异常的处理
except:
其中 init() 函数会根据命令行解析的参数,预设大量的配置信息,如下:
def init():
"""
Set attributes into both configuration and knowledge base singletons
based upon command line and configuration file options.
"""
_useWizardInterface() # --wizard 启动引导模式,设置conf.levle/conf.risk等参数
setVerbosity() # -v 设置默认的日志输出详细度
_saveConfig() # --safe 保存当前扫描的配置
_setRequestFromFile() # -r(解析BurpLog/WebScarabLog)解析 request file 的文件内容
_cleanupOptions() # 为 conf 中的参数赋初值
_cleanupEnvironment()
_purge() #--purge 清除指定目录下 sqlmap 相关信息
_checkDependencies() #--dependencies 检查是否缺失依赖
_createHomeDirectories() # 创建 output、history 目录
_createTemporaryDirectory() # --tmp-dir 创建临时目录
_basicOptionValidation() # 验证部分参数值是否符合预期
_setProxyList() # --proxy-file
_setTorProxySettings() # --tor
_setDNSServer() # --dns-domain
_adjustLoggingFormatter() # 初始化日志格式化工具
_setMultipleTargets() # -l 解析 burp log 的文件内容
_listTamperingFunctions() # --list-tampers 输出 tamper文件夹 的tamper脚本详细信息
_setTamperingFunctions() # --tamper 设置后续自定义要调用的 tamper
_setPreprocessFunctions() # --preprocess 设置请求前预处理发送的函数 在自定py脚本中定义preprocess函数
_setPostprocessFunctions() # --postprocess 设置请求后处理响应的函数 在自定py脚本中定义postprocess函数
_setTrafficOutputFP() # -t 保存所有 HTTP 流量记录到指定文本文件
_setupHTTPCollector() # --har 将所有 HTTP 流量记录到一个 HAR 文件中
_setHttpChunked() # --chunked 使用 HTTP 分块传输编码(POST)请求
_checkWebSocket() # 如果目标(-u)是wss://|ws:// ,检查websocket 环境是否正常
parseTargetDirect() # -d 解析数据库链接 (mysql://user:)
# 根据下面的配置文件设置相关信息用于构造HTTP包
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)):
_setHostname() # 设置 conf 中的 hostname
_setHTTPTimeout() # --timeout 设置请求最大超时时间
_setHTTPExtraHeaders() # --headers 设置请求的 headers
_setHTTPCookies() # --cookie 设置请求的 cookies
_setHTTPReferer() # --referer 设置请求的 referer
_setHTTPHost() # --host 设置请求的 host
_setHTTPUserAgent() # -A 设置请求的 UA
_setHTTPAuthentication() # --auth-* 设置请求的认证信息
_setHTTPHandlers() # 设置proxy代理
_setDNSCache() # 设置 dns 缓存
_setSocketPreConnect()
_setSafeVisit() # --safe-* 避免因太多失败请求引发会话销毁 https://sqlmap.kvko.live/usage/request#bi-mian-yin-tai-duo-shi-bai-qing-qiu-yin-fa-hui-hua-xiao-hui
_doSearch() # -g 处理 Google Dork 解析
_setStdinPipeTargets() # 从 pipeline 中获取 targets
_setBulkMultipleTargets() # -m 从文本中获取 targets
_checkTor() # --tor* 检查 tor 代理
_setCrawler() # --crawl-* 设置爬虫信息
_findPageForms() # --forms 寻找页面中的表单
_setDBMS() # --dbms 设置 DBMS
_setTechnique() # --technique 设置检测类型
_setThreads() # --threads
_setOS() # --os
_setWriteFile() # --file-write
_setMetasploit() # --os-pwn/--os-smb/--os-bof
_setDBMSAuthentication() # --dbms-cred 设置 DBMS 的认证信息
loadBoundaries() # 加载 Boundaries
loadPayloads() # 加载 Payloads
_setPrefixSuffix() # --prefix/--suffix 注入 payload 的前/后缀字符串
update() # 更新 sqlmap
_loadQueries() # 加载 queries(xml文件)
之后则会调用 start() 函数正式进入 sql 注入检测流程。
2. 整理目标站点信息
在 start() 函数中存在多种方式:1. 传入 crack 参数进行 Hash 文件破解。2. 直连数据库。3. 尝试进行 sql 注入。接下来分析的是第三种模式进行 sql 注入探测。
2.1. 收集 target 信息 (start 函数)
在初始化结束后, start() 函数首先会开始 target 目标信息,例如解析参数等操作,下面说明分析一些重要函数的功能。
2.1.1. initTargetEnv
在该函数中,会通过正则匹配,检测 URL, Post-data 和 HTTPHeaders 中的参数,判断是否有被用户手动定义注入点。
# sqlmap/lib/core/target.py
def initTargetEnv():
# ...
# 判断参数中是否有关键字来识别需要注入的点:"%INJECT_HERE%"
# INJECT_HERE_REGEX = r"(?i)%INJECT[_ ]?HERE%"
match = re.search(INJECT_HERE_REGEX, "%s %s %s" % (conf.url, conf.data, conf.httpHeaders))
# ...
2.1.2. parseTargetUrl
解析 url 参数信息
2.1.3. setupTargetEnv
用于设置目标信息,例如生成站点相关文件夹用于保存后续信息/从文件中恢复上下文:
2.1.4. _setRequestParams
作用是将解析 URL 将所有需要注入的参数存放到 conf.parameters 全局变量中。sqlmap 首先是判断用户是否自定义了参数,如果没有则会自动从 GET/POST, HTTP Headers 等参数中自动提取可注入点。
2.1.5. checkconnection
sqlmap 会对目标 ur 进行检查,判断是否能够成功访问,无法进行访问会直接跳过对该 target 的 sql 注入测试。访问成功则会将页面内容保存到全局变量 kb.pageTemplate 和 kb.originalPage 中。
- sqlmap 是通过 Request.queryPage() 进行收发数据包的,sqlmap 会首先通过
checkconnection()对 target 进行首次访问,主要用于确认 target 是否能够成功访问,并且保存 target 站点的信息,例如正常访问时的响应页面,target 可能有的指纹信息(服务器类型,操作系统,WAF 等信息) - 获取到 target 信息后,如果用户在 sqlmap 的调用参数中还设置了 regex 或 string 参数用于精准识别正常响应页面,sqlmap 会拿响应内容同 regex/string 进行比较,判断是否是期望的页面。
- 最后如果自身没有设置 cookie,但是服务器返回了 cookie 值,sqlmap 会询问你是否需要使用这些 cookie 值。
2.1.6. checkWaf
在判断 target url 可访问后(checkconnection),sqlmap 调用 checkWaf() 判断是否存在 WAF。
有以下几种情况会跳过 checkWaf:
@stackedmethod
def checkWaf():
"""
Reference: http://seclists.org/nmap-dev/2011/q2/att-1005/http-waf-detect.nse
"""
# string|notString|regexp 未设置默认字符串进行匹配
# conf.dummy 在测试请求时使用的“虚拟”参数
# conf.offline 离线测试从已有的请求文件中加载数据进行测试
# 设置了skipWaf
if any((conf.string, conf.notString, conf.regexp, conf.dummy, conf.offline, conf.skipWaf)):
return None
# 原始页面状态就是404
if kb.originalCode == _http_client.NOT_FOUND:
return None
然后会正式开始进行 WAF 测试:
- 首先会使用内置的高危语句,拼接为一个新的参数,然后发送给服务器尝试触发 WAF 拦截。
IPS_WAF_CHECK_PAYLOAD = "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')#" payload = "%d %s" % (randomInt(), IPS_WAF_CHECK_PAYLOAD) place = PLACE.GET if PLACE.URI in conf.parameters: value = "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload)) else: value = "" if not conf.parameters.get(PLACE.GET) else conf.parameters[PLACE.GET] + DEFAULT_GET_POST_DELIMITER # 随机生成一段randstr()=payload进行参数拼接 value += "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload))
在获取到响应内容后会将该页面的内容同 checkconnection 中设置的默认页面 kb.pageTemplate 通过 comparison 函数计算页面的差异率,如果差异率小于 IPS_WAF_CHECK_RATIO = 0.5 , 则判断存在 WAF。 sqlmap 则会推荐你使用 tamper 脚本。
2.1.7. checkNullConnection
sqlmap 的参数中有 --null-connection ,可通过不接收响应包,只通过分析响应头的方式,来判断 sql 注入,主要的目的是用来节省带宽。如果设置了该函数,则会调用 checkNullConnection() 函数。
在 checkNullConnection 中,sqlmap 判断 target 是否支持 null-connection 的方式有两种:
- 通过 HEAD 的方式,判断响应 Header 中是否存在
Content-Length字段并且确实无响应包(某些服务器可能不遵守 HTTP 标准规范)。 - 如果通过 HEAD 的方式 target 仍返回响应体,但是如果 Header 中使用
Range: bytes=-1该字段,依旧能够影响 target 响应体的输出,那么依旧能够使用null-connection这种方法。
在校验完之后,如果能够使用该方式,sqlmap 会将对应的null-connection方式(NULLCONNECTION.HEAD或NULLCONNECTION.RANGE)保存在kb.nullConnection中,从而影响调用 Request.QueryPage 时的发送数据包方式。
参考链接:
- http://www.wisec.it/sectou.php?id=472f952d79293
2.2. 检查待注入参数
- sqlmap 首先会将待注入的参数进行优先级排序,注入点检测顺序优先级依次是
CUSTOM_POST>CUSTOM_HEADER>URI>POST>GET。 - 然后再根据 level 等级过滤 需要注入的参数,剩下的属于将要测试注入的参数。
- 如果开启了布尔盲注测试,sqlmap 还有调用 checkDynParam 函数对参数进行校验,判断是否处于动态参数,如果此时开启了
--skip-static参数,则那些判定为静态的参数则不会再尝试进行 sql 注入判断,即testSqlInj=False。 - 然后以
(conf.hostname, conf.path, place, parameter)作为 key 值记录到kb.testedParams中。
2.2.1. checkDynParam
检测参数是否属于动态参数比较简单,直接通过随机生成的数值替换掉原先的参数,判断页面是否发生了变化来判断是否属于动态参数。
def checkDynParam (place, parameter, value):
"""
This function checks if the URL parameter is dynamic. If it is
dynamic, the content of the page differs, otherwise the
dynamicity might depend on another parameter.
"""
if kb.choices.redirect:
return None
kb.matchRatio = None
dynResult = None
randInt = randomInt()
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
infoMsg = "testing if %sparameter '%s' is dynamic" % ("%s " % paramType if paramType != parameter else "", parameter)
logger.info(infoMsg)
try:
# 直接生成随机生成的randInt值替换掉原先的value值
payload = agent.payload(place, parameter, value, getUnicode(randInt))
dynResult = Request.queryPage(payload, place, raise404=False)
except SqlmapConnectionException:
pass
# 只有返回的页面内容原先内容不同时,判断该参数为动态参数可注入。
result = None if dynResult is None else not dynResult
kb.dynamicParameter = result
return result
3. sql 注入点检测
这个 sqlmap 开始逐个对参数进行尝试注入:
- 首先时调用 heuristicCheckSqlInjection 函数尝试对参数进行启发性注入探测,简单的判断是否存在 sql 注入,XSS 或者文件包含漏洞。
- 再调用 checkSqlInjection 函数对注入点进行检测,判断是否可能存在注入。
4. sql 注入利用-action
在发现注入点后,sqlmap 会尝试进行 sql 注入获取一些基础信息
DBMS Handle
在获取到注入点之后,后续的注入都由 DBMS Handler 类 接管。sqlmap 会自动获取数据库的基础信息,而之后的 sql 注入利用则都是由 DBMS Handler 的根据用户输入决定。
sqlmap 的 payload 生成过程
从测试 payload 模板到发送给 target 的真实 payload, sqlmap 有一系列的处理过程,下面是对处理过程的简单分析。
- sqlmap 的 payload 主要是几种:1. 硬编码在代码中的 2. 位于 XML 文件中 (vector, request, response 等标签中)。这些 payload 语句会有一些特殊字符,例如
[RANDNUM],INFERENCE等 (大部分是在 cleanupPayload 函数中处理的)。 - 这些 payload 是无法直接发送,还需要进行处理。因此经常能够看到 sqlmap 在调用 Request.queryPage 函数将真实 payload 发送给 targert 前,都会通过下面的几个的函数进行处理。
fstPayload = agent.cleanupPayload(test.request.payload, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) # ... query = agent.prefixQuery(fstPayload) query = agent.suffixQuery(query) payload = agent.payload(newValue=query) Request.queryPage(payload, place, raise404=False)- 首先是 agent.cleanupPayload 函数,该函数主要用于替换 payload 中的特殊字符。
- 接着是 prefixQuery 和 suffixQuery 函数会补上对应的测试中/注入成功记录的 boudary 对应的 prefix, suffix 和 comment 内容。
- 最后是 payload 函数,该函数的作用主要是:1. 调用 tamper 脚本对内容进行处理 2. 对 base64 标记的内容进行编码 3. 根据 where 的类型,会对 originalValue 进行一些变换(取负等)
- 最后得到即使可以发送的 payload, 然后调用 queryPage 进行发送。