SQLMAP 源码通读(上)

一、前情提要

最近实验室大佬在写一个渗透测试框架,目的是在出去做渗透测试项目的时候能够快速发现漏洞,以及实施快速打击。

但无论大小型的扫描器、扫描框架等,结果的真实性、内容丰富性和效率、效用都会形成一个矛盾,因此,能够性价比最高的发现问题都是扫描框架首要的任务。

对于检测注入这个位置,我们希望能够快速发现,不需要实际注入得出数据,然后想着正好学习一下sqlmap的源码,能够使用的部分直接拿过来,再根据实际作用改一改。于是就有了这篇文章。对于我的水平应该是99%都看不懂的,但是好在网上相关文章不少。

SQLMAP不用过多介绍,是一款检测和利用SQL注入的工具,基于python编写。

SQL注入 5 种类型:布尔注入,时间注入,报错注入,联合查询注入,堆查询注入。还有一种SQLMAP支持的,内联注入。

二、SQLMAP使用方法

先来看一下SQLMAP的常见使用方法和参数。

帮助信息:-h/–hh/–version

-h    能够显示帮助信息,–hh 能够显示更多的信息

–version    能够显示程序版本

日志显示等级:-v

  • 0:只显示Python的回溯、错误和关键消息;
  • 1:同时显示基本信息和警告信息;
  • 2:同时显示调试信息;
  • 3:同时显示注入的payload;
  • 4:同时显示HTTP请求;
  • 5:同时显示HTTP响应头;
  • 6:同时显示HTTP响应页面页面。

指定目标(GET方法):-d/-u/–url/-g

-d      表示sqlmap将自己作为客户端去连接数据库

-u/–url     指定目标系统的 URL

-g     对Google的搜索结果进行SQL注入探测,例如:sqlmap.py -g “inurl:”.php?id=1.”

指定参数(GET方法):-p/–batch/–force-ssl

-p     对指定的参数进行SQL注入检测

–batch     不与使用者进行信息交互,直接执行

–force-ssl     强制使用 SSL/HTTPS 协议

指定目标(POST方法):-r/-l/-c

-r     将HTTP请求文件保存到文本文档中,使用-r参数读取文本文件的参数进行SQL注入

-l     将burpsuite log文件保存到文本文档中,使用参数-l读取文本文档的参数进行SQL注入

-c     对配置ini文件进行SQL注入探测

指纹信息:-f/–fingerprint/-b/–banner

-b/–banner     获取数据库版本信息和数据库类型

-f/–fingerprint     查询目标系统的数据库管理系统的指纹信息

枚举:-a/–all/–dbs/–current-user/–privileges-U/–roles/–current-db/–users/–passwords/–tables/–columns/–schema/–dump/–dump-all/-D/-T/-C/–exclude-sysdbs/–count/–schema/–batch/–sql-query/–sql-shell

-a/–all     获取所有信息

–dbs     枚举DBMS中所有的数据库

–current-user     获取当前用户

–privileges-U     username(当前账号/CU) 查看当前账号的权限

–roles     列出数据库管理员角色,仅适用于当前数据库是Oracle

–current-db     获取当前数据库

–users     枚举DBMS用户

–passwords     枚举DBMS用户密码hash值

–tables     枚举DBMS数据库管理系统中的表

–columns     枚举DBMS数据库管理系统中的列

–schema     枚举DBMS数据库管理系统的模式

–dump     转储DBMS数据库表项,后面加-C表示转储某列,-T转储某表,-D转储某数据库,–start,–stop,–first,–last指定开始结束,开头结尾。

–dump-all     转储所有的DBMS数据库表项

-D     指定枚举的DBMS中的数据库

-T     指定要枚举的表

-C     指定要枚举的列

-D 数据库名 –tables     查找指定数据库中的表

-D 数据库名 -T 表名 –columns     查找指定数据库的某个表中的列

–exclude-sysdbs     忽略掉系统数据库

–count     查找表中的记录数

–schema     查找数据库的架构,包含所有的数据库,表和字段,以及各自的类型,一般与–exclude-sysdbs共用

–batch     默认每次自动执行

–sql-query/–sql-shell     运行自定义的SQL语句,例:–sql-query=”select * from users;”所得到的内容被保存到dump目录中

指定数据:–data/–cookie/–param-del/–user-agent/–random-agent/–host/–referer/–method

–data=DATA     指定post数据包中被传输的值,例:sqlmap.py -u “http//www.xxx.com” –data=”name=123&pass=456″ -f

–cookie=COOKIE     指定cookie值登录web程序,并且会尝试自动注入cookie值,需要在level 2或者大于level 2等级才会进行cookie注入。如果cookie被服务器端更新,那么sqlmap也会自动更新cookie值。例:sqlmap -u “http://www.xxx.com/index/?id=1&Submit=Submit#” -p id –cookie=”security=low; PHPSESSID=d806c1f76f24a9687640ce497afc8f20″ –batch

–param-del      告知sqlmap变量分隔符。web程序一般默认是&符号作为分隔符,如果并非&,则需要指定变量分隔符,例:sqlmap.py -u “http://www.xxx.com” –data=”user=123;pass=456″ –param-del=”;” -f

–user-agent     指定UA头部信息。sqlmap默认使用UA为:sqlmap/1.0-dev-版本号 http://sqlmap.org

–random-agent     使用sqlmap/txt/user-agents.txt字典中的UA头部进行随机替换

–host=host header     指定host头部信息,当level为5的时候才会检测host值

–referer=REFERER     指定Referer头部信息,当level大于等于三 ,才回去检测referer头部是否存在注入

–method=GET/POST     指定使用get或者POST方式发送数据,默认以get方式发送

注入模块配置:-p/–skip/-u/–dbms/–os/–invalid-bignum/–invalid-logical/–no-cast/–tamper

-p     指定扫描的参数,也可以指定HTTP头部字段,例:sqlmap.py -u “http://www.xxx.com/?id=1″ -p “User-Agent,Referer,id”

–skip     跳过对某些参数进行测试。当使用–level的值很大但是有个别参数不想去测试的时候使用–skip去跳过 例:sqlmap.py -u “http://www.xxx.com/?id=1″ –skip “User-Agent,Referer,id”

-u     设置URL注入点。当有些网站将参数和值一起加入到URL链接中,sqlmap是默认不对其进行扫描的,所以我们需要去指定对某个参数值进行注入,例:sqlmap.py -u “http://www.xxx.com/param1/value1*/param2/value2*” –dbms 设置目标服务器所使用的DBMS,例:–dbms=”mysql”

–os     指定目标的操作系统,例:–os=”linux”

–invalid-bignum     给参数值给与最大值让其失效

–invalid-logical     使用布尔判断使取值失效

–no-cast     榨取数据时,sqlmap将所有的结果转换成字符串,并用空格替换null值(老版本mysql数据库需要开启此开关)

–tamper=TAMPER     使用给定的脚本去混淆绕过应用层的过滤,比如waf/ids等。该文件存放在/sqlmap/tamper文件下,例:sqlmap.py -u “www.xxx.com/?id=1” -p “id” –tamper=”between.py,overlongutf8more.py,lowercase.py “

身份认证:–auth-type/–auth-cred/–auth-file

–auth-type=AUTH      指定HTTP认证类型(Basic, Digest, NTLM or PKI)

–auth-cred=AUTH     指定HTTP认证证书(格式为:name:password),例:sqlmap.py -u “http://www.xxx.com/?id=1” –auth-type=Basic –auth-cred “user:pass”

–auth-file=AUTH     指定HTTP认证PEM格式的证书/私钥文件

代理类:–proxy/–proxy-cred/–ignore-proxy/–tor/–tor-type

–proxy=”http://127.0.0.1:8081”     设置代理服务器

–proxy-cred=”name:pass”     例:sqlmap -u “http://www.xxx.com/?id=1” –proxy=”http://127.0.0.1:8081” –proxy-cred=”user:pass” -f

–ignore-proxy     忽略系统级代理设置,通常用于扫描本地网络目标

–tor     使用匿名网络进行注入

–tor-type     指定tor网络连接类型

延时类:–delay/–timeout/–retries/–randomize/–scope

–delay=DELAY     每次HTTP(S)请求之间延迟时间,值为浮点数,单位为秒,默认无延迟

–timeout=TIMEOUT     设置超时时间,默认30秒

–retries=RETRIES     设置重连次数,默认3次

–randomize     设置随机改变的参数值

–scope      利用正则表达式过滤日志内容

技术类型:–technique/–time-sec/–union-cols/–union-char/–union-from/–dns-domain/–second-order

SQLMAP对于以下参数具有默认值:

–technique=TECH     指定sqlmap使用的检测技术,默认情况下会测试所有的方式。

–time-sec=TIMESEC     设置延迟时间,基于时间的注入检测默认延迟时间是5秒

–union-cols=UCOLS      联合查询时默认是1-10列,当level=5时会增加到测试50个字段数,可以使用此参数设置查询的字段数。

–union-char=UCHAR     默认情况下sqlmap针对UNION查询的注入会使用NULL字符;

–union-from=UFROM     在UNION查询SQL注入的FROM部分中使用的表

–dns-domain=DNS     攻击者控制了某DNS服务器,使用此功能可以提高数据查询的速度

–second-order=URL     使用此参数指定到哪个页面获取响应判断真假,–second-order后面跟一个判断页面的URL地址。

CSRF TOKEN 绕过:–csrf-url/–csrf-token

–csrf-url=URL     获取token的链接

–csrf-token=param     你提交注入包中,token参数

检测类:–level/–risk/–string/–not-string/–Regexp/–code

–level     共有5级,默认等级1

–risk     共有4级,默认等级1,risk升高将使用update等方法测试,将造成数据被篡改等风险

–string     指定页面返回某个字符串则为真

–not-string     指定页面不返回某个字符串则为真

–Regexp     当查询的值为真时,使用正则表达式去匹配

–code     当查询的值为真时,执行HTTP code

系统文件操作:–file-read/–file-write

–file-read=RFILE     从后端DBMS文件系统中读取文件(读取系统文件),例:–file-read=”/etc/passwd”

–file-write=SHELL.PHP –file-dest=DFILE     把当前系统的文件写入到目标服务器的某个目录下去

OS系统访问:–os-cmd/–os-shell/–os-pwn/–os-smbrelay/–os-bof

–os-cmd     运行任意操作系统命令(适用于数据库为mysql,postgresql,或Sql Server,并且当前用户有权限使用特定的函数),例:–os-cmd id :执行id命令,后期是与sqlmap进行交互,生成UDF函数在操作系统下执行命令

–os-shell     获取一个shell(目标系统为管理员权限,并且得知绝对路径)

–os-pwn      获取一个OOB shell,meterpreter或VNC

–os-smbrelay     一键获取一个OOB shell,meterpreter或VNC

–os-bof     存储过程缓冲区溢出利用

UDF注入:–udf-inject/–shared-lib

UDF:自定义函数,利用UDF函数达到执行操作系统命令

–udf-inject     注入用户自定义函数

–shared-lib=SHLIB     指定共享库的本地路径

两条命令需一起使用

爆破表/字段名:–common-tables/–common-columns

使用场景:

  • mysql版本<5.0的时候,没有information_schema库
  • mysql版本>=5.0,但无权读取information_schema库
  • 微软的access数据库,默认无权读取MSysObjects库。

–common-tables     爆破表名,例:sqlmap.py -u “http://www.baidu.com/?id=1” –common-tables

–common-columns     暴力破解列名

Windows注册表:–reg-read/–reg-add/–reg-del/–reg-key/–reg/value/–reg-data/–reg-type

–reg-read      读取注册表的值

–reg-add      写入注册表值

–reg-del     删除注册表值

–reg-key,–reg-value,–reg-data,–reg-type     注册表辅助选项

一般性参数:-s/-t/–charset/–crawl/–csv-del/–dbms-creb/–flush-session/–fresh-queries/–hex/–save

-s     指定sqlite会话文件保存位置

-t     记录流量文件保存位置

–charset     强制字符编码,例:–charset=GBK

–crawl     从开始位置爬站深度,例:–crawl=3

–csv-del dump     下来的数据以CVS格式保存

–dbms-creb     指定数据库账号

–slush-session     清空session

–fresh-queries     忽略session查询结果

–hex     当dump下非ASCii字符内容时,将其编码成16进账形式,收到后解析还原

–save     将命令保存成配置文件

WAF对抗类:–check-waf/–identify-waf/–hpp

–check-waf     检测WAF/IPS/IDS

–hpp     绕过WAF/IPS/ISD,尤其是对ASP/IIS和ASP.NET/IIS有效

–identify-waf     彻底的WAF/IPS/IDS检测,支持三十多种产品

优化性能:-o/–predict-output/–keep-alive/–null-connection/–threads

-o     开启所有优化开关

–predict-output     预测常见的查询输出

–keep-alive     使用持久的HTTP(S)连接

–null-connection     从没有实际的HTTP响应体中检索页面长度

–threads=THREADS     设置最大的HTTP(S)请求并发量(默认为1)

其他参数:–safe-url/–safe-freq/–skip-urlencode/–eval/–mobile/–purge-output/–smart/–wizard

–safe-url=SAFEURL     指定需要去重复扫描的地址

–safe-freq     指定每发送多少次的注入请求之后接着发正常请求,有些web应用程序会在攻击者多次访问错误的请求时屏蔽掉以后的所有请求,所以设置这两个参数防止以后无法进行注入 –skip-urlencode 跳过URL编码的载荷,默认在get请求中是需要对传输数据进行编码,但是有些web服务器不遵守RPC标准编码,使用原始字符提交数据,所以使用这个参数使sqlmap不使用URL编码的参数进行测试

–eval=EBALCODE     在请求之前执行提供的python代码,例:sqlmap.py -u “http://www.xxx.com/?id=1&hash=c4ca4238a0b923820dcc509a6f75849b” –evel=”import hashlib;hash=hashlib.md5(id).hexdigest()”

–mobile     模拟智能手机设备,修改User-Agent为手机端的UA

–purge-output     清空output文件夹

–smart     当有大量检测目标时,只修改基于错误的检测结果

–wizard     设置用户向导参数,教你一步步针对目标注入

三、源码阅读

基本上比较常用的参数都在上面列了一下,当个速查表吧,然后就开始学习sqlmap源码之路吧。

本次学习的源码版本为 1.3.6.50#dev

目录结构

首先看一下目录结构,我们将文档类的文件排除掉,只看系统类的:

主目录

文件/文件夹说明
data/数据库注入检测载荷、用户自定义攻击载荷、字典、shell命令、数据库触发顺序等
extra/一些额外功能,例如发出声响(beep)、运行cmd、安全执行、shellcode等
lib/包含了sqlmap的多种连接库,如五种注入类型请求的参数、提权操作等。
plugins/数据库信息和数据库通用事项
tamper/绕过脚本
thirdparty/sqlmap使用的第三方的插件
sqlmap.confsqlmap的配置文件,如各种默认参数(默认是没有设置参数、可设置默认参数进行批量或者自动化检测)
sqlmap.pysqlmap主程序文件
sqlmapapi.pysqlmap的api文件,可以将sqlmap集成到其他平台上
swagger.yamlapi文档

程序初始化

main()函数首先是执行5个函数,我们先依次看下每个函数的作用。

补丁包:dirtyPatches()

对于程序的一些问题及修复,写成了补丁函数,优先执行。

首先设定了 httplib 的最大行长度,接下来导入第三方的 windows 下的 ip地址转换函数模块,然后对编码进行了一些替换,把 cp65001 替换为 utf8 避免出现一些交互上的错误,这些操作对于 sqlmap 的实际功能影响并不是特别大,属于保证起用户体验和系统设置的正常选项,不需要进行过多关心

SQLMAP 源码通读(上)

解决交叉引用:resolveCrossReferences()

为了消除交叉引用的问题,一些子程序中的函数会被重写,在这个位置进行赋值

SQLMAP 源码通读(上)

检查环境:checkEnvironment()

如名,函数的主要作用是检查环境

SQLMAP 源码通读(上)

首先调用modulePath(),获取程序路径,判断程序是否由 py2exe 打包成 exe ,因为打包后无法使用 __file__ 获得路径,为了防止乱码,使用getUnicode返回unicode编码的路径

SQLMAP 源码通读(上)

然后使用LooseVersion判断python版本号,版本号小于1.0则报错并退出

接下来是对pip安装环境的补丁,可以看到是一些系统环境变量的设置

SQLMAP 源码通读(上)

这里面定义个贯穿sqlmap全局的三个全局变量,cmdLineOptions,conf ,kb

绝对路径设置:setPaths()

配置程序文件的路径,并且判断.txt, .xml, .zip为扩展名的文件是否存在并且是否可读,这部分将预设的程序全部使用的信息都加载进来了,在后续的调用中就去各个变量、字典里取就可以了

SQLMAP 源码通读(上)

paths值为一个sqlmap自己封装的字典类型 AttribDict

SQLMAP 源码通读(上)

为什么作者要自己封装一个字典类型呢?首先能看到的就是,可以直接使用访问属性的方式访问键值

SQLMAP 源码通读(上)

其次是作者定义了__deepcopy__,为了解决字典赋值传递后浅copy会修改原数据的问题

SQLMAP 源码通读(上)

BANNER展示:banner()

函数判断执行参数中是否包含 –version 或 –api参数,或者在配置中是否将disableBanner中设置为True,如果都没有,那就将BANNER字符串赋值给 “_” ,

SQLMAP 源码通读(上)

跨终端显示颜色则使用了第三方插件: colorama 库。跟一下变量BANNER

SQLMAP 源码通读(上)

使用random.sample随机选择,然后使用正则替换,再跟一下BANNER,最后我们看到的就是SQLMAP的字符画了

SQLMAP 源码通读(上)

实际上这部分看出来SQLMAP中间红色部分是随机的。。用了这么久竟然不知道。。

SQLMAP 源码通读(上)

五个函数执行之后进入之后的流程,在 banner 打印之后,就是对命令行参数的操作了

SQLMAP 源码通读(上)

cmdLineOptions 同样是一个 AttribDict,跟进 cmdLineParser

SQLMAP 源码通读(上)

可以看到,对命令行参数的处理使用了 optparse,首先将获取到的命令行参数选项进行判断和拆分以后转变成dict键值对的形式存入到cmdLineOptions,然后传入initOption进行下一步操作

SQLMAP 源码通读(上)

_setConfAttributes() 函数初始化了一些重要的属性值为空

_setKnowledgeBaseAttributes() 函数同样的初始化了一些重要的属性值到“知识库”中

_mergeOptions(inputOptions, overrideOptions) 函数主要的作用是将配置项中的参数和命令行获得的参数选项以及缺省选项进行合并,函数执行完毕将会将字典 mergedOptions 的值进行更新

检查是否通过中间进行标准输入:

SQLMAP 源码通读(上)

检查是否调用api,如果是将会引入新的包,并重写sys.stdout和sys.stderr

SQLMAP 源码通读(上)

判断过后打印法律声明和时间

SQLMAP 源码通读(上)

然后就是执行 init() 函数进行程序的初始化,对用户定义参数进行配置和初始化。这部分后面再说。

init() 函数执行过后就是执行测试以及start函数的执行了,我们看到了三个测试函数 smokeTest、vulnTest 和 liveTest 用于测试。

在不进行测试的情况下,导入包文件,在这一步才导入包,会使程序的启动更快。

然后判断 conf.profile ,这个程序调用的图形处理,暂时没弄明白是干嘛的。默认为空,程序直接走到else,启用start() 程序,也在后面说。

SQLMAP 源码通读(上)

接下来是一大长串的 except 和最终的 finally

SQLMAP 源码通读(上)

finally中包括了一些提示信息、清除临时目录文件、清除线程、清除配置等等一些文件

程序的最终结尾又是一个 if 判断,如果命令行参数里有sqlmapShell的话重新执行main函数,这实际上用一个死循环来完成的类似长连接的效果

SQLMAP 源码通读(上)

命令行参数处理

文件位置:/lib/core/option.py

init() 函数将命令行参数和配置文件的选项值同时设置为程序配置

具体做了什么如下:

def init():
    _useWizardInterface() # --wizard 用户向导程序,为了给初学者一个友好的界面
    setVerbosity() # -v 设置debug等级
    _saveConfig() # --save 将命令行选项保存到sqlmap配置INI文件中
    _setRequestFromFile() # -r 从文件中设置http请求
    _cleanupOptions() # 清理配置选项
    _cleanupEnvironment() # 清理环境
    _purge() # 安全删除(清除)sqlmap数据目录
    _checkDependencies() # 检测丢失的依赖
    _createHomeDirectories() # 在sqlmap的主目录中创建目录
    _createTemporaryDirectory() # 创建运行的临时目录
    _basicOptionValidation() # 检测选项是否有效
    _setProxyList() # --proxy 设置代理列表
    _setTorProxySettings() # --tor/--tor-type 设置Tor代理
    _setDNSServer() # --dns-domain 设置DNS服务器
    _adjustLoggingFormatter() # 调整日志格式
    _setMultipleTargets() # 如果在多个目标中运行,只定义一种参数
    _listTamperingFunctions()
    _setTamperingFunctions() # --tamper 设置tamper脚本
    _setPreprocessFunctions() # --preprocess 从给定的脚本加载预处理函数
    _setTrafficOutputFP() # 设置记录http日志文件
    _setupHTTPCollector() # 清理http收集
    _setHttpChunked()
    _checkWebSocket() # 检测websocket-client模块调用
 
    parseTargetDirect() # 解析目标DBMS
 
    if any((conf.url, conf.logFile, conf.bulkFile, conf.sitemapUrl, conf.requestFile, conf.googleDork, conf.liveTest)):
        # 如果设置了上述选项,配置相关http
        _setHostname() # 设置hostname
        _setHTTPTimeout() # 设置超时时间
        _setHTTPExtraHeaders() # 设置Headers
        _setHTTPCookies() # 设置Cookies
        _setHTTPReferer() # 设置Referer
        _setHTTPHost() # 设置Host
        _setHTTPUserAgent() # 设置UserAgent
        _setHTTPAuthentication() # 设置HTTPAuthentication
        _setHTTPHandlers() # 检查并设置所有HTTP请求的 HTTP/SOCKS代理。
        _setDNSCache() # 设置DNS缓存
        _setSocketPreConnect()
        _setSafeVisit() # 检查并设置安全访问选项。
        _doSearch() # 使用搜索平台搜索结果并存储
        _setBulkMultipleTargets()
        _setSitemapTargets() # 解析SitemapTargets中的目标
        _checkTor() # 检测Tor
        _setCrawler() # 设置爬虫
        _findPageForms() # 寻找网页的表单
        _setDBMS() # 强制DBMS选项 
        _setTechnique()
 
    _setThreads() # 设置线程
    _setOS() # 强制OS
    _setWriteFile() # 写入文件
    _setMetasploit() # Metasploit相关设置
    _setDBMSAuthentication() # 设置数据库身份认证,来使用另一个身份执行
    loadBoundaries() # 载入Boundaries
    loadPayloads() # 载入Payloads
    _setPrefixSuffix() # 设置前后缀
    update() # 更新sqlmap
    _loadQueries() # 加载查询的xml /查询

初始和解析HTTP相关内容

文件位置:/lib/controller/controller.py

start() 函数将调用函数,检查URL的连接稳定性,以及对所有的 GET/POST/Cookie/User-Agent 参数进行检查,检查他们是否为动态的,以及是否收到SQL注入的影响

SQLMAP 源码通读(上)

接下来是检测目标点是否存在SQL注入,conf.direct 参数为True,则用户配置直连数据库,将绕过下方的流程,直接检测:

  • initTargetEnv() 用于初始化目标环境
  • setupTargetEnv() 用于设置目标环境
  • action() 函数用于在受影响的URL参数上执行SQL注入攻击,并尝试提取 DBMS 或 操作系统相关信息
SQLMAP 源码通读(上)

正常流程是将配置 conf 字典中的目标URL、方法、data、cookie中添加到 kb.targets 中,然后进一步判断,如果没有就报错,然后打印出目标个数

SQLMAP 源码通读(上)

然后是判断网络连接数

SQLMAP 源码通读(上)

设置HTTP连接的一些参数,都是python数据类型转换,没什么好说的

initTargetEnv() 函数主要就是完成全局变量 conf 和 kb 的初始化工作

parseTargetUrl() 函数主要通过正则完成针对目标网址的解析工作,如获取协议名、路径、端口、请求参数等信息

SQLMAP 源码通读(上)

测试过的 url 参数信息会保存到 kb.testedParams 中,所以在进行test之前,会先判断当前的url是否已经test过,如果没test过的话,则 testSqlInj = True,否则 testSqlInj = False,如果值为False ,到后续就不会再执行注入攻击了,因此接下来这段代码就是在判断目前要测试的点是否测试过,测试过是否为注入点等,并修改 testSqlInj 的值,最后进行判断是否要跳过这个点

SQLMAP 源码通读(上)

接下来是判断是否存在多个目标,依次与用户交互确认是否进行测试

SQLMAP 源码通读(上)

然后执行 setupTargetEnv() 函数,该函数主要包含3个子功能:

  • 创建保存目标执行结果的目录和文件
  • 将 get 或 post 发送的数据解析成字典形式,并保存到 conf.paramDict 中
  • 读取session文件(如果存在的话),并读取文件中的数据,保存到 kb 变量中
SQLMAP 源码通读(上)

检查连接、是否有用户自定义的 String(字符)或 Regexp(正则):

SQLMAP 源码通读(上)

然后是 checkWaf() 函数,检测是否有WAF,之后再说

检查空连接(nullConnection)、检查页面稳定性,以及对参数、测试列表进行排序

什么是 nullConnection?根据官方手册,是一种不用获取页面内容就可以知道页面大小的方法,这种方法在布尔盲注中有非常好的效果,可以很好的节省带宽。

如果启用 –null-connection,计算页面相似率就只是很简单的通过页面的长度来计算,这部分后面会提到

SQLMAP 源码通读(上)

然后根据参数中的 level 级别判断是否进行 cookie 、referer、ua的注入

使用函数 checkDynParam() 判断参数是否是动态,如果不是动态则需要选取其他参数。其核心是给参数另外一个随机值,然后通过选择参数及对于页面的各种规则的判断,计算出两个页面的相似比率,来判定是否为动态。

SQLMAP 源码通读(上)

根据level等级不同的情况判断是否应该跳过参数

SQLMAP 源码通读(上)

如果参数为动态,将接下来进入检测SQL注入测试环节,程序将调用 heuristcCheckSqlInjection() 函数进行启发式检测,再得到 POSITIVE 的结果后,再调用 checkSqlInjection() 进行注入点检查,再次确认有漏洞且不是 FALSE_POSTTIVE后,将injectable 设为True,中间产生的数据放入相应的数据集中,执行下一步,这两个函数的内容也会稍后详细说。

SQLMAP 源码通读(上)

如果没检查出漏洞点,将根据不同情况提供各种各样的建议参数

SQLMAP 源码通读(上)

否则将结果进行保存

SQLMAP 源码通读(上)

二次确认有注入数据后,将与用户交互确认,并执行 action() 函数,才是开始跑数据了。这部分后续会详细介绍。

SQLMAP 源码通读(上)

中间又是一大堆的 except 异常处理之后,finally 中显示了http错误代码,并提示最大连接限制的情况

SQLMAP 源码通读(上)

最后提示结果文件名

SQLMAP 源码通读(上)

识别waf

文件位置:/lib/controller/checks.py

在看源码之前发现 checkWaf() 函数上使用了装饰器,@stackedmethod,使用pushvalue/popvalue函数的方法(用于堆栈重新对齐的回退函数),这句话就没看明白,无所谓,接着看

如果用户使用了一些自定义的参数,或指定不进行waf检测,将会直接return,不进行waf检测

SQLMAP 源码通读(上)

第一步判断原始状态码,不存在就return。如果之前数据库有数据就取出返回,

SQLMAP 源码通读(上)

payload是一个随机数字和一个专门用来检测WAF的payload——IPS_WAF_CHECK_PAYLOAD

SQLMAP 源码通读(上)

IPS_WAF_CHECK_PAYLOAD 位于 /lib/core/settings.py 中

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')#"

可以看到真的是肆意的想触发WAF规则,然后将payload拼入参数值中,并根据是否重定向进行配置。

SQLMAP 源码通读(上)

然后使用 queryPage() 函数判断前后两次访问的相似度,如果(ratio)比率小于阈值(0.5),那说明访问未被拦截,就认为不存在Waf

SQLMAP 源码通读(上)

结果存入数据库,并根据结果进行用户交互

SQLMAP 源码通读(上)

测试 payload 之后,将会开始检测 WAF 指纹,sqlmap 识别 waf 指纹的插件位于 /thirdparty/identywaf/identYwaf.py

全局搜索一下引入这个插件的文件,发现只有在 /lib/request/basic.py 中的 processResponse() 函数进行了引用,来看一下这个函数

parseResponse() 函数对可能的数据库后端进行检测

SQLMAP 源码通读(上)

是一个 if 判断,processResponseCounter 和 IDENTYWAF_PARSE_LIMIT进行比较,只有前者小于后者的时候才能继续执行

SQLMAP 源码通读(上)

IDENTYWAF_PARSE_LIMIT 在配置文件中默认为 10,而 processResponseCounter 在每次调用 processResponse 时加一 ,可想而知,在计数到时的时候,下面的流程将失效

SQLMAP 源码通读(上)

然后调用了 identYwaf.non_blind_check(),for 循环遍历 non_blind 中的数据,判断是否在 kb.identifiedWafs 中,并打印出识别 waf 结果

SQLMAP 源码通读(上)

我们先来看一下non_blind_check() 函数实现什么功能

中将会拿到原始的相应包,并进行正则匹配,(图里的print是我自己加上去的,汗)

如果正则匹配到了,将会加入到 non_blind 里

SQLMAP 源码通读(上)

而 waf 的指纹在同目录的 data.json 中,通过 load_data() 函数加入到 WAF_RECOGNITION_REGEX 变量中

SQLMAP 源码通读(上)

在断点调试的过程中发现了识别的过程,拿到原始的数据包,使用正则进行匹配,如果匹配到了就将数据写入变量,后续去变量里找数据并打印

data.json 长成这样,这部分又是我们可以自行拓展的部分

SQLMAP 源码通读(上)

但是究竟在哪个位置开始进行waf识别的呢?

再次跟一下 processResponse() 的调用链

发现在 /lib/request/connect.py 中的 getPage() 函数中调用,sqlmap每次获取返回结果都要走这个函数,而在这个函数中是一定能调到 processResponse() 的

SQLMAP 源码通读(上)

也就是说, sqlmap 通过一句 PAYLOAD 检测目标URL是否有waf,通过前十次连接返回的结果进行正则匹配,识别 waf 指纹

启发性检查

文件位置:/lib/controller/checks.py

首先走到的是 heuristcCheckSqlInjection(),翻译过来应该是启发性注入检查,说的好听,实际上就是利用简单的payload根据返回结果尝试性的猜想是否可能出现问题

SQLMAP 源码通读(上)

跟一下这个函数,首先进行判断,我也不知道 heavy dynamic 是啥意思,暂时就叫他强动态吧

SQLMAP 源码通读(上)

从 conf 里拿数据,声明用户自定义的前缀、后缀

SQLMAP 源码通读(上)

按照规则随机生成一些字符

SQLMAP 源码通读(上)

拼接 payload ,并再次调用 queryPage() 计算请求的相似性

SQLMAP 源码通读(上)

同样使用 FORMAT_EXCEPTION_STRINGS 全局配置,通过查找字符串的方式判断几次请求之间的差异

parseFilePaths() 检测页面之中可能出现的绝对路径

SQLMAP 源码通读(上)

wasLastResponseDBMSError() 检测是否出现数据库错误信息

SQLMAP 源码通读(上)

检测在输入非常规的参数值时,服务端是否返回格式化字符串出错的信息

SQLMAP 源码通读(上)

此处出现条件判断,当 casting 为 False ,参数为动态,且参数原始值为数字类型时,代表格式化字符串没有出错信息,可以看到函数中的 payload 是利用数值型运算来判定页面相似度,例如原来的页面是 id=5 ,程序随机出一个数字,比如是 3 ,就再次访问 id=8-3 ,返回结果与 id=5 使用 queryPage() 函数进行相似度判断

SQLMAP 源码通读(上)

如果结果 result 为 False,证明二者页面相似度较低,再次生成非数字 payload,例如 id=5.aaa ,再次访问页面计算页面相似度,并赋值给 casting,如果两者页面相似度较高,则 casting为 True

再次出现条件判断,当 casting 为 True 时,根据 URL split 拆分得到可能的后端语言,并根据不同语言给出可能的处理参数方式,此处有两种可能走到这个位置,一个是格式化参数出现报错信息,一个是 id=5 和 id=5.aaa 页面返回的结果是一样的,这种情况可以基本判断服务端处理参数的方式。

SQLMAP 源码通读(上)

如果 result 为True,也就是数值运算的相似性结果很高,使用 getErrorParseDBMSes() 获取可能的后端数据库类型,否则返回消息

SQLMAP 源码通读(上)

注入检测完毕后按照全局配置重新随机生成随机值,进行检测XSS和文件包含漏洞

SQLMAP 源码通读(上)

简单的检测了下XSS漏洞,原理就是如果我们输入的 value 原封不动的出现在了响应里,可能是服务器直接将我们的字符串放在了html页面的某个位置,这种情况下就可能存在XSS漏洞,不得不说,这种方式检测XSS,误报率真的有点。。。

SQLMAP 源码通读(上)

之后又简单的检测文件包含漏洞,实际上就是如果测试的参数点对应文件,而我们注入的payload找不到文件的话可能会报错

SQLMAP 源码通读(上)

也是使用全局配置中的正则表达式,可以看到这个正则真的是比较随意,而且对中文环境也十分不友好,不过对于sqlmap来讲就是顺便的事,重点也不是检测这个漏洞

SQLMAP 源码通读(上)

验证SQL注入点

文件位置:/lib/controller/checks.py

这个点是检测是否存在SQL注入的核心函数 checkSqlInjection() ,前期检测和验证是否是注入点,这个函数800多行代码,好就好在作者注释写的还不少,我们先来看一下主逻辑

前期是初始化一些参数,首先要把要处理的 payload 和 boundaries 加载进来,boundaries是所谓的边界值,也就是真正payload的前缀和后缀信息,这部分是根据已知的参数来筛选 boundaries

SQLMAP 源码通读(上)

paramType 是注入的类型,如GET/POST

getSortedInjectionTests() 通过从错误消息中检测到的DBMS 返回优先测试列表,包含了每个测试项的名称,赋值给 tests

这些数据都是和 /data/xml/payloads/ 目录下每个类型相对应的 xml ,在程序初始化 loadPayloads() 时已经加载了

我们先来看一下payload到底长什么样,随便打开一个 payload的xml文档

SQLMAP 源码通读(上)

每个text标签包含的信息有

  • title 测试的名称,用于日志输出
  • stype 表示注入的类型,sqlmap支持了六种注入的类型,值分别为 1-6
  • level 级别值为 1-5
  • risk 风险,可能对数据库造成的风险,值为 1-3
  • clause 这个字段表明对应的测试 payload 适用于哪种类型的 SQL 语句。一般来说,很多语句并不一定非要特定 WHERE 位置的。 0: Always 1: WHERE / HAVING 2: GROUP BY 3: ORDER BY 4: LIMIT 5: OFFSET 6: TOP 7: Table name 8: Column name 9: Pre-WHERE (non-query)
  • where 如何添加我们的完整payload,值为 1-3 ,1:在原值后面添加payload 2:将原值替换为不存在的随机字串然后添加payload 3:用我们的payload替换原始值
  • vector 这个payload大概长什么样,在实际测试中,由于前缀、后缀,tamper脚本等原因发送的payload不一定完完全全是这个
  • request 关于发起请求的设置与配置。在这些配置中,有一些是特有的,但是有一些是必须的,例如 payload 是肯定存在的,但是 comment 是不一定有的,char 和 columns 是只有 UNION 才存在
  • payload 实际测试使用的 Payload
  • comment 注释,添加在后缀之前
  • char 只有 UNION 注入存在的字段,用于爆破字段数量
  • columns 只有 UNION 注入存在的字段,用于查询字段范围
  • response 如何确认注入payload成功
  • comparison 针对布尔盲注的特有字段,表示对比和 request 中请求的结果
  • grep 针对报错型注入特有字段,使用正则表达式去匹配结果
  • time 针对时间盲注,等待多少秒结果返回
  • union 处理 UNION 注入的方法
  • details 如果 response 标签中的检测结果成功了,可以推断出什么结论
  • dbms 是什么数据库
  • dbms_version 数据库版本
  • os 操作系统版本

在了解到了这些 payload 的 xml 结构之后,我们对于检测的流程也有了初步的判断,payloads 文件夹下对应的每个 xml 都是一种注入类型对应的测试配置

SQLMAP 源码通读(上)

然后是一个长达700行的while循环

首先是判断后端数据库有没有检测到,如果有,会进行用户交互跳过其他类型的检测,如果没有,会用布尔型盲注来检测数据库

SQLMAP 源码通读(上)

函数为 heuristicCheckDbms() ,这个用于测试的布尔型盲注非常简单,核心原理是构造一个

(SELECT "[RANDSTR1]" [FROM_DUMMY_TABLE.get(dbms)])='[RANDSTR1]'

(SELECT '[RANDSTR1]' [FROM_DUMMY_TABLE.get(dbms)])='[RANDSTR2]'

通过二者的正负关系来判断数据库类型,代码如下:

SQLMAP 源码通读(上)

FROM_DUMMP_TABLE 中储存了不同数据库独有的表

SQLMAP 源码通读(上)

当然数据库类型检测并不是必须的,因为 sqlmap 实际工作中,如果没有指定 DBMS 则会按照当前测试 Payload 的对应的数据库类型去设置。

实际上在各种 Payload 的执行过程中,会包含着一些数据库的推断信息,如果 Payload 成功执行,这些信息可以被顺利推断则数据库类型就可以推断出来。

识别到DBMS后,与用户交互确认

SQLMAP 源码通读(上)

实际上sqlmap会根据下面他支持漏洞类型来生成payload检测注入

  • B: Boolean-based blind SQL injection(布尔型注入)
  • E: Error-based SQL injection(报错型注入)
  • U: UNION query SQL injection(可联合查询注入)
  • S: Stacked queries SQL injection(可多语句查询注入)
  • T: Time-based blind SQL injection(基于时间延迟注入)
  • Q: inline_query(内联查询)
SQLMAP 源码通读(上)

然后又是一长串判断什么时候跳过,什么时候执行什么,根据用户输入不同巴拉巴拉,先不看了

中间拼接字符串的地方也直接绕过

SQLMAP 源码通读(上)
SQLMAP 源码通读(上)

我们直接来看下针对不同类型注入的检测

布尔型注入

对于布尔型注入,简单理解为,正常请求 id=1 ,正逻辑请求 id=1 or 1=1 ,负逻辑请求 id=1 or 1=2

我们要比对不同情况下页面的变化来判定是否此页面存在漏洞,接下来

生成用于页面对比的payload

SQLMAP 源码通读(上)

然后经过了几个对比,这里我就不截代码了

  • 负逻辑与正常请求对比
  • 负逻辑、正常请求、和启发性测试的页面是否不同

如果正逻辑与正常页面相同,且与负逻辑页面不同,也没有 nullConnection 配置,再次进行负逻辑请求,并与正常请求页面对比。

除了正逻辑、负逻辑与正常页面的对比,sqlmap还会发送错误的请求,并与正常页面进行比较,再次确认服务器的响应。

在多重对比后,sqlmap会确认注入点是否存在并将结果返回。这部分的代码逻辑比较复杂,建议动态调试加抓包详细分析一下。

报错注入

报错注入的识别最为简单,都是基于正则匹配的检测手段,这一点的payload中的 response 标签的 grep子标签中就能体现出来。

SQLMAP 源码通读(上)

而在在 output 判断的位置,连续用三个 or 来对齐最终答案,也就是说,在多个判断维度中,有一个命中,就会判断为存在报错注入

  • 页面错误内容
  • HTTP 的错误页面
  • Headers 中的内容
  • 重定向信息

基于时间的盲注

基于时间的盲注其实也很好理解,我们通常是 sleep 几秒,然后看看服务器返回的时间有没有变长

SQLMAP 源码通读(上)

对于这一点,在代码中我们看到了 trueResult、falseResult 等标志位,判断过程在 queryPage 中完成

在 queryPage() 中经过一系列的条件判断、生成标志位,最终如果需要基于时间的对比,会调用wasLastResponseDelayed()

SQLMAP 源码通读(上)

这个函数是 sqlmap 用来判断是否相应是否延迟的函数

SQLMAP 源码通读(上)

作者贴心的给出了注释和数学引用连接:

99.999999997440%的 非基于时间的SQL注入 的响应时间应在 正负 7*标准差内([正常响应时间])

也就是说,sqlmap会取这个点的相应时间,做标准差,再用基于时间注入的 payload 做对比

最理想的情况是做标准差的数据应该大于等于30 ,这个参数 MIN_TIME_RESPONSES 在setting.py 中配置

SQLMAP 源码通读(上)

我们还需要设定几个参数

  • TIME_STDEV_COEFF 标准差系数,按照文献中说明的应该大于等于7,salmap默认为7
  • MIN_VALID_DELAYED_RESPONSE 能够被认为可能是 delay 的最小时间

然后出现了一处用户交互,询问我们是否要优化延迟时间

SQLMAP 源码通读(上)

如果选 Y 将会调用 adjustTimeDelay() 函数根据程序定义的算法计算合适的time delay

SQLMAP 源码通读(上)

联合查询注入

联合查询的主代码较少,但实际上联合查询是逻辑最复杂的,而且也是最经典的

SQLMAP 源码通读(上)

这部分的逻辑实际上可以想想我们最开始学习SQL注入的过程,使用order by 猜解列数,然后用类似 union select 1,2,3,4,5,6 猜测输出点在列中的位置,然后的响应的位置执行注入语句获得想要的信息

而sqlmap也是实现这个流程,具体函数使用 unionTest() ,我们跟进去

SQLMAP 源码通读(上)

核心函数 _unionTestByCharBruteforce() ,继续跟

SQLMAP 源码通读(上)

函数通过 _findUnionCharCount() 猜列数,通过 _unionConfirm() 确认存在 Union 注入

猜列数我们刚才说了,是使用 order by 进行,猜解,这部分在_findUnionCharCount() 中定义了函数 _orderByTest() 来实现

SQLMAP 源码通读(上)

同样的,在拼接完整的 payload 之后,使用 queryPage 与原始页面进行比较来获取列数,这部分代码实际上能看出来使用了二分法来进行比较,也是为了节约程序处理时间

当 order by 失效时,将会使用函数 forgeUnionQuery() 直接UNION SELECT 不同的列数,再根据结果区分哪一个是正确的列数

至于中间的算法,就暂时略过

找到了列数后,接下来就是应该寻找输出点,基础逻辑也很简单,我们只需要将 UNION SELECT NULL,NULL,….,NULL 中的NULL依次替换,然后在结果中寻找被我们插入的随机的字符串,就可以容易的定位到输出点的位置,这部分功能由函数 _unionConfirm() 实现

对于几种注入检测过后,是一堆用户键盘退出异常处理

SQLMAP 源码通读(上)

最后返回注入结果,并使用函数 checkFalsePositive() 检查误报,

SQLMAP 源码通读(上)

在确认无误报后,使用 checkSuhosinPath() 检查 Suhosin或类似的防御机制,不知道为啥在这个位置针对这个单独做了一个检测。先不管了。

然后使用 checkFilteredChars() 检查过滤字符。 将injections返回。

到此为止,前期全部的检测该点是否具有SQL注入漏洞的过程就完成了,按照流程来走的话,下一步就是 action() 函数,就开始正式执行注入攻击行为了。



原创文章,作者:su18,如若转载,请注明出处:http://absec.cn/?p=846

发表评论

电子邮件地址不会被公开。 必填项已用*标注

联系我们

010-61943626

在线咨询:点击这里给我发消息

邮件:marketing@anbai.com

工作时间:电话:周一至周五,10:00-18:30,节假日休息,邮件随时发哦~