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.txttxt\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

  1. 完整的 sqlmap 各参数如下: https://github.com/sqlmapproject/sqlmap/wiki/Usage

Sqlmap 文件目录

Sqlmap 调试环境

  1. 用 phpstudy 搭建 sqli-labs 环境
  2. 使用 sqlmap 进行测试
    1. 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
    2. 测试不同的 SQL 注入方式:
      - 布尔注入 (boolean_blind)
      - 报错注入 (error_based)
      - 内联查询 (inline_query)
      - 堆查询 (stacked_queries)
      - 延时注入 (time_blind)
      - 联合查询(union)

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.pageTemplatekb.originalPage 中。

  1. sqlmap 是通过 Request.queryPage() 进行收发数据包的,sqlmap 会首先通过 checkconnection() 对 target 进行首次访问,主要用于确认 target 是否能够成功访问,并且保存 target 站点的信息,例如正常访问时的响应页面,target 可能有的指纹信息(服务器类型,操作系统,WAF 等信息)
  2. 获取到 target 信息后,如果用户在 sqlmap 的调用参数中还设置了 regex 或 string 参数用于精准识别正常响应页面,sqlmap 会拿响应内容同 regex/string 进行比较,判断是否是期望的页面。
  3. 最后如果自身没有设置 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 测试:

  1. 首先会使用内置的高危语句,拼接为一个新的参数,然后发送给服务器尝试触发 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 的方式有两种:

  1. 通过 HEAD 的方式,判断响应 Header 中是否存在 Content-Length 字段并且确实无响应包(某些服务器可能不遵守 HTTP 标准规范)。
  2. 如果通过 HEAD 的方式 target 仍返回响应体,但是如果 Header 中使用 Range: bytes=-1 该字段,依旧能够影响 target 响应体的输出,那么依旧能够使用 null-connection 这种方法。
    在校验完之后,如果能够使用该方式,sqlmap 会将对应的 null-connection 方式(NULLCONNECTION.HEADNULLCONNECTION.RANGE)保存在 kb.nullConnection 中,从而影响调用 Request.QueryPage 时的发送数据包方式。

参考链接:

2.2. 检查待注入参数

  1. sqlmap 首先会将待注入的参数进行优先级排序,注入点检测顺序优先级依次是 CUSTOM_POST > CUSTOM_HEADER > URI > POST > GET
  2. 然后再根据 level 等级过滤 需要注入的参数,剩下的属于将要测试注入的参数。
  3. 如果开启了布尔盲注测试,sqlmap 还有调用 checkDynParam 函数对参数进行校验,判断是否处于动态参数,如果此时开启了 --skip-static 参数,则那些判定为静态的参数则不会再尝试进行 sql 注入判断,即 testSqlInj=False
  4. 然后以 (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 开始逐个对参数进行尝试注入:

  1. 首先时调用 heuristicCheckSqlInjection 函数尝试对参数进行启发性注入探测,简单的判断是否存在 sql 注入,XSS 或者文件包含漏洞。
  2. 再调用 checkSqlInjection 函数对注入点进行检测,判断是否可能存在注入。

4. sql 注入利用-action

在发现注入点后,sqlmap 会尝试进行 sql 注入获取一些基础信息

DBMS Handle

在获取到注入点之后,后续的注入都由 DBMS Handler 类 接管。sqlmap 会自动获取数据库的基础信息,而之后的 sql 注入利用则都是由 DBMS Handler 的根据用户输入决定。

sqlmap 的 payload 生成过程

从测试 payload 模板到发送给 target 的真实 payload, sqlmap 有一系列的处理过程,下面是对处理过程的简单分析。

  1. sqlmap 的 payload 主要是几种:1. 硬编码在代码中的 2. 位于 XML 文件中 (vector, request, response 等标签中)。这些 payload 语句会有一些特殊字符,例如 [RANDNUM], INFERENCE 等 (大部分是在 cleanupPayload 函数中处理的)。
  2. 这些 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)
    
    1. 首先是 agent.cleanupPayload 函数,该函数主要用于替换 payload 中的特殊字符。
    2. 接着是 prefixQuerysuffixQuery 函数会补上对应的测试中/注入成功记录的 boudary 对应的 prefix, suffix 和 comment 内容。
    3. 最后是 payload 函数,该函数的作用主要是:1. 调用 tamper 脚本对内容进行处理 2. 对 base64 标记的内容进行编码 3. 根据 where 的类型,会对 originalValue 进行一些变换(取负等)
  3. 最后得到即使可以发送的 payload, 然后调用 queryPage 进行发送。

参考链接