作者:@v1ll4n(安全研发工程师,现就职于长亭科技 )
一直在想准备一系列 sqlmap 的文章,担心会不会因为太老太旧了被大家吐槽,思前想后也查了一些现有的资料,还是准备出一部分关于 sqlmap 关键技术细节的探讨。同时也在对其核心的讨论中,提炼出一些思想与方法。
我相信在阅读本文的读者中,很大一部分人都是曾经尝试阅读过 sqlmap 源码的同学。但是在实际阅读的时候,我们发现大家总是存在各种各样奇葩的困难与困惑。
“SqlMap 源码为什么会有大几百行一千行的方法啊”“它里面 conf 和 kb 又是啥?这两个全局变量里面到底存了啥?”“为什么我直接把 sqlmap 的 xml 取出来,还是并不是特别方便使用他们的 payload”
我相信这些疑问大家肯定第一次在阅读这个项目的事,都会遇到。实际上,并不是因为 sqlmap 项目的水平高导致大家看不懂。而是由于项目背负了太多的历史包袱,导致在接近十年的发展中,开发者与后期维护者并没有对这款工具进行重构与大规模重写,反而是继续使用 python2 对其缝缝补补。
在本系列文章中,我们主要针对 sqlmap 的最核心的方方面面进行分析,本文主要针对基础流程进行介绍与描述,本文由非常细致的 sqlmap 源码解读,希望有需要的读者可以从中受益。
0x00 准备工作
想要阅读 sqlmap 源码我相信大家的选择肯定更多的是从 github 下直接 clone 代码到本地,直接使用本地编辑器或者 IDE 打开直接来分析。所以基本操作也就是
git clone https://github.com/sqlmapproject/sqlmap
cd sqlmap
当然很多读者是 Python3 用户,其实也没有必要费很大力气在本机上安装 Python2 然后再进行操作。笔者使用的环境是
- Mac OS X
- Pyenv
- VSCode
推荐使用 Pyenv(+virtualenv) 构建 Python 环境运行 sqlmap。
0x01 初始化与底层建筑
笔者当然可以直接指出所有的重要逻辑在什么位置,但是这样并不好。这样做的后果就是大家发出奇怪的疑问:
它里面 conf 和 kb 又是啥?这两个全局变量里面到底存了啥?
逐步熟悉整个项目的构建和项目中贯穿全局的两个奇怪的全局变量,对于加速理解 sqlmap 的核心逻辑起了很大的作用。在笔者的工作和实践中,确实是很有感触。
所以我们还是从头看起吧!
我们在上图中,可以找到很明显的程序命令行入口,我们暂且只分析命令行入口所以,我把无关的东西全部打了马赛克,所以接下来我们看到 main
函数直接来了解
我相信大家看到了上图应该就知道我们主要应该看 try
中的内容。实际上 except
中指的是 sqlmap 中各种各样异常处理,包含让程序退出而释放的异常/用户异常以及各种预期或非预期异常,在 finally
中,大致进行了数据库(HashDB)的检查/恢复/释放以及 dumper
的收尾操作和多线程的资源回收操作。具体的不重要的代码我们就不继续介绍了,接下来直接来了解比较重要的部分吧。
在实际在工作部分中,我们发现了 1-4 函数对环境和基础配置进行了一同操作,然后在 5 步骤的时候进行步骤初始化,然后开始启动 sqlmap。实际上这些操作并不是一无是处,接下来有详有略介绍这些步骤究竟发生了什么。
-
在 DirtyPatches 中,首先设定了 httplib 的最大行长度(
httplib._MAXLINE
),接下来导入第三方的 windows 下的 ip地址转换函数模块(win_inet_pton
),然后对编码进行了一些替换,把cp65001
替换为utf8
避免出现一些交互上的错误,这些操作对于 sqlmap 的实际功能影响并不是特别大,属于保证起用户体验和系统设置的正常选项,不需要进行过多关心。 -
在环境检查中,做了如下操作:检查模块路径,检查 Python 版本,导入全局变量。我们可能并不需要关心太多这一步,只需要记得在这一步我们导入了几个关键的全局变量:
("cmdLineOptions", "conf", "kb")
,需要提醒大家的是,直接去lib.core.data
中寻找这几个变量并不是明智的选择,因为他们并不是在这里初始化的(说白了就是找到了定义也没有用,只需要知道有他们几个就够啦)。 -
初始化各种资源文件路径。
-
打印 Banner。
-
这一部分可以说是非常关键了,虽然表面上仍然是属于初始化的阶段,但是实际上,如果不知晓这一步,面对后面的直接对全局变量
kb
和conf
的操作将会变的非常奇怪和陌生。在这步中,我们进行了配置文件初始化,知识库(KnowledgeBase初始化)以及用户操作的Merge
和初始化。我们在之后的分析中如果遇到了针对kb
和conf
的操作,可以直接在这个函数对应的lib.core.option
模块中寻找对应的初始化变量的定义。当然,这一步涉及到的一些kb/conf
的 fields 也可能来源于lib.parse.cmdline
中,可以直接通过ctrl+F
搜索到
-
中主要包含所有初始变量的初始值,这些初始值在
init()
的设定主要是引用各种各样的函数来完成基础设置,我们没有必要依次对其进行分支,只需要用到的时候知道回来寻找就可以了。 -
冒烟测试,测试程序本身是否可以跑得通。
-
功能测试,测试 sqlmap 功能是否完整。
进入上一段代码的条件是 if not conf.updateAll
,这个是来源于 lib.parse.cmdline
中定义的更新选项,如果这个选项打开,sqlmap 会自动更新并且不会执行后续测试步骤和实际工作的步骤。
在实际的启动代码中,笔者在上图中标注了两处,我们在使用命令行的时候,更多的是直接调用 start() 函数,所以我们直接跟入其中寻找之后需要研究的部分。
0x02 测试前的目标准备
当我们找到 start() 函数的时候,映入眼帘的实际上是一个很平坦的流程,我们简化一下,以下图代码为例:
我们仍然看到了 conf
中一些很奇怪的选项,针对这些选项我们在 0x01 节中强调过,可以在某一些地方找到这些选项的线索,我们以 conf.direct
为例,可以在 lib.parse.cmdline
中明确找到这个选项的说明:
根据说明,这是直连数据库的选项,所以我们可能暂时并不需要关心他,我们暂时只关注 sqlmap 是如何检测漏洞的,而不关心他是怎么样调用数据库相关操作的。
接下来稍有一些想法的读者当然知道,我们直接进行第四部分针对这个目标循环的分析是最简单有效的办法了!
好的,接下来我们就打开最核心的检测方法:
进入循环体之后,首先进行检查网络是否通断的选项,这个选项很容易理解我们就不多叙述了;确保网络正常之后,开始设置 conf.url,conf.method,conf.data,conf.cookie
和 headers 等字段,并且在 parseTargetUrl()
中进行各种合理性检查;之后会根据 HTTP 的 Method 提取需要检查的参数;随后如果当前启动时参数接受了多个目标的话,会在第4步中做一些初始化的工作。
在完成上述操作之后,执行 setupTargetEnv()
这个函数也是一个非常重要的函数,其包含如下操作:
def setupTargetEnv():
_createTargetDirs()
_setRequestParams()
_setHashDB()
_resumeHashDBValues()
_setResultsFile()
_setAuthCred()
setRequestParams()
都是关于本身存储(缓存)扫描上下文和结果文件的。当然我们最关注的点肯定是 setRequestParams()
这个点。在深入了解这一个步骤之后,我们发现其中主要涉及到如下操作:
所以我们回归之前的 start()
方法中的 foreach targets 的循环体中,在 setupTargetEnv()
之后,我们现在已经知道了关于这个目标的所有的可以尝试注入测试的点都已经设置好了,并且都存在了 conf.paramDict
这个字典中了。
至此,在正式开始检测之前,我们已经知道,conf.url, conf.method, conf.headers ...
之类的包含基础的测试的目标的信息,在 conf.paramDict
中包含具体的不同位置的需要测试的参数的字典,可以方便随时渲染 Payload。关于其具体的行为,其实大可不必太过关心,因为我们其实并不需要具体的处理细节,这些细节应该是在我们遇到问题,或者遇到唔清楚的地方再跳出来在这些步骤中寻找,并且进行研究。
0x03 万事俱备
可以说在读者了解上面两节讲述的内容的时候,我们就可以正式探查真正的 SQL 注入检测时候 sqlmap 都坐上了什么。其实简单来说,需要经过下面步骤:
笔者通过对 controller.py
中的 start()
函数进行分析,得出了上面的流程图。在整个检测过程中,我们暂且不涉及细节;整个流程都是针对检查一个目标所要经历的步骤。
checkWaf
在 checkWaf()
中,文档写明:Reference: http://seclists.org/nmap-dev/2011/q2/att-1005/http-waf-detect.nse
,我们可以在这里发现他的原理出处,有兴趣的读者可以自行研究。在实际实现的过程中代码如下:
笔者在关键部分已经把标注和箭头写明,方便大家理解。我们发现 payload
这个变量是通过随机一个数字 + space + 一个特制 Payload(涉及到很多的关于敏感关键词,可以很容易触发 WAF 拦截)。
随即,sqlmap 会把 payload 插入该插入的位置:对于 GET 类的请求,sqlmap 会在之前的 query 语句后面加入一个新的参数,这个参数名通过 randomStr()
生成,参数的值就是经过处理的 Payload。如果有读者不理解,我们在这里可以举一个例子:
如果我们针对
http://this.is.a.victim.com/article.php?id=1
http://this.is.a.victim.com/article.php?id=1&mbjwe=2472%20AND%201%3D1%20UNION%20ALL%20SELECT%201%2CNULL%2C%27%3Cscript%3Ealert%28%22XSS%22%29%3C/script%3E%27%2Ctable_name%20FROM%20information_schema.tables%20WHERE%202%3E1--/%
2A%2A/%3B%20EXEC%20xp_cmdshell%28%27cat%20../../../etc/passwd%27%29%23
我们似乎遇到一些问题
有心的读者可能发现,我们在上小节出现了一个神奇陌生的词 Page Ratio, 这个词其实在整个 sqlmap 中是非常重要的存在,我们之后会在后续的文章中详细介绍这部分理论。
0x04 然后呢
其实我们当然可以继续讲解每一个函数都做了什么,但是限于篇幅问题,我们可能要先暂停一下了;与此同时,我们本文的内容“基础流程”实际上已经介绍完了,并且引出了我们需要在下一篇文章介绍的概念之一“Page Ratio”。
所以接下来我们可能要结束本文了,但是我更希望的是,每一个读者都能够尝试自己分析,自己去吃透 sqlmap 的细节。
0x05 结束语
感谢读者的耐心,在接下来的文章中,笔者将会更加深入介绍 sqlmap 最核心的算法和细节处理。