SQLMAP源码分析Part1:流程篇

0x00 概述

1.drops之前的文档 SQLMAP进阶使用介绍过SQLMAP的高级使用方法,网上也有几篇介绍过SQLMAP源码的文章曾是土木人,都写的非常好,建议大家都看一下。
2.我准备分几篇文章详细的介绍下SQLMAP的源码,让想了解的朋友们熟悉一下SQLMAP的原理和一些手工注入的语句,今天先开始第一篇:流程篇。
3.之前最好了解SQLMAP各个选项的意思,可以参考sqlmap用户手册和SQLMAP目录doc/README.pdf
4.内容中如有错误或者没有写清楚的地方,欢迎指正交流。有部分内容是参考上面介绍的几篇文章的,在此一并说明,感谢他们。

0x01 流程图

enter image description here

0x02 调试方法

1.我用的IDE是PyCharm。
2.在菜单栏Run->Edit Configurations。点击左侧的“+”,选择Python,Script中选择sqlmap.py的路径,Script parameters中填入注入时的命令,如下图。 enter image description here

3.打开sqlmap.py,开始函数是main函数,在main函数处下断点。 enter image description here

4.右键Debug ‘sqlmap’,然后程序就自动跳到我们下断点的main()函数处,后面可以继续添加断点进行调试。如下图,左边红色的代表跳转到下一个断点处,上面红色的表示跳到下一句代码处

enter image description here

5.另外,如果要在代码中加中文注释,需要在开始处添加以下语句:#coding:utf-8。

0x03 流程

3.1 初始化

我这里用的版本是:1.0-dev-nongit-20150614
miin()函数开始73行:

进入common.py中的setPaths()函数后,就可以看到这个函数是定义SQLMAP路径和文件的,类似于:

接下来的78行函数initOptions(cmdLineOptions),包含了三个函数,作用如流程图所示,设置conf,KB,参数. conf会保存用户输入的一些参数,比如url,端口
kb会保存注入时的一些参数,其中有两个是比较特殊的kb.chars.start和kb.chars.stop,这两个是随机字符串,后面会有介绍。

3.2 start

102行的start函数,算是检测开始的地方.start()函数位于controller.py中。

首先这四句,意思是,如果你使用-d选项,那么sqlmap就会直接进入action()函数,连接数据库,语句类似为:

上面代码会把url,methos,data,cookie加入到kb.targets,这些参数就是我们输入的

enter image description here

接下来从274行的for循环中,可以进入检测环节

此循环先初始化一些一些变量,然后判断之前是否注入过,如果没有注入过,testSqlInj=True,否则testSqlInj=false。后面会进行判断是否检测过。

372行setupTargetEnv()函数中包含了5个函数,这些函数作用是

1.创建输出结果目录

2.解析请求参数

3.设置session信息,就是session.sqlite。

4.恢复session的数据,继续扫描。

5.存储扫描结果。

6.添加认证信息

其中比较重要的就是session.sqlite,这个文件在sqlmap的输出目录中,测试的结果都会保存在这个文件里。

3.2.1 checkWaf

377行checkWaf()是检测是否有WAF,检测方法是NMAP的http-waf-detect.nse,比如页面为index.php?id=1,那现在添加一个随机变量index.php?id=1&aaa=2,设置paoyload类似为AND 1=1 UNION ALL SELECT 1,2,3,table_name FROM information_schema.tables WHERE 2>1– ../../../etc/passwd,如果没有WAF,页面不会变化,如果有WAF,因为payload中有很多敏感字符,大多数时候页面都会发生改变。
接下来的conf.identifyWaf代表sqlmap的参数–identify-waf,如果指定了此参数,就会进入identifyWaf()函数,主要检测的waf都在sqlmap的waf目录下。

enter image description here

当然检测的方法都比较简单,都是查看返回的数据库包种是否包含了某些特征字符。如:

回到start函数,385行会判断是否注入过,如果还没有测试过参数是否可以注入,则进入if语句中。如果之前测试过,则不会进入此语句。

这中间sqlmap给了我们一些注释,可以看到,level>=3时,会测试user-agent,referer,level>=5时,会测试HOST,level>=2时,会测试cookie。当然最终的测试判断还要在相应的xml中指定,后面会介绍。
480行的checkDynParam()函数会判断参数是否是动态的,比如index.php?id=1,通过更改id的值,如果参数是动态的,页面会不同。

3.2.2 heuristicCheckSqlInjection

 

502行有个heuristicCheckSqlInjection()函数,翻译过来是启发性sql注入测试,其实就是先进行一个简单的测试,设置一个payload,然后解析请求结果。
heuristicCheckSqlInjection()在checks.py中,821行开始如下:

首先conf.prefix和conf.suffix代表用户指定的前缀和后缀;在while ‘\” not in randStr中,随机选择'”‘, ‘\”, ‘)’, ‘(‘, ‘,’, ‘.’中的字符,选10个,并且单引号要在。接下来生成一个payload,类似u’name=PAYLOAD_DELIMITER\__1).”.”.”\’.”__PAYLOAD_DELIMITER’。其中PAYLOAD_DELIMITER\__1和__PAYLOAD_DELIMITER是随机字符串。请求网页后,调用parseFilePaths进行解析,查看是否爆出绝对路径,而wasLastResponseDBMSError是判断response中是否包含了数据库的报错信息。

上面的代码是从888行开始,DUMMY_XSS_CHECK_APPENDIX = “<‘\”>”,如果输入的字符串在页面中返回了,会提示可能存在XSS漏洞。

enter image description here

接下来,我们回到start函数中,继续看下面的代码。

在502行判断testSqlInj,如果为true,就代表之前没有检测过,然后就会到checkSqlInjection,checkSqlInjection()才是真正开始测试的函数,传入的参数是注入方法如GET,参数名,参数值。我们跟进。

3.2.3 checkSqlInjection

checkSqlInjection()在checks.py中,91行开始

paramType是注入的类型,如GET。tests是要测试的列表,如下图所示,包含了每个测试项的名称,这些数据都是和/sqlmap/xml/payloads/目录下每个xml相对应的。

enter image description here

101行开始,这段代码主要是判断DBMS类型,首先,如果用户没有手工指定dbms,则会根据页面报错或者bool类型的测试,找出DBMS类型,找出后,会提示是否跳过测试其他的DBMS。然后,对于测试出来的DBMS,是否用所有的payload来测试。

enter image description here

140行if stype == PAYLOAD.TECHNIQUE.UNION:会判断是不是union注入,这个stype就是payload文件夹下面xml文件中的stype, 如果是union,就会进入,然后配置列的数量等,今天先介绍流程,union注入以后会介绍。

177行,就是用户提供的–technique,共有六个选项BEUSTQ,但是现在很多文档,包括SQLMAP的官方文档都只给了BEUST的解释说明,少个inline_query,相当于查询语句中再加入一个查询语句。

接下来,就是生成payload的过程。288行:

test.request.payload为’AND [RANDNUM]=[RANDNUM]'(相应payload.xml中的request值)。根据此代码,生成一个随机字符串,如fstPayload=u’AND 2876=2876’。
302行:

循环遍历boundaries.xml中的boundary节点,如果boundary的level大于用户提供的level,则跳过,不检测。
307行:

首先,循环遍历test.clause(payload中的clause值),如果clauseTest在boundary的clause中,则设置 clauseMatch = True,代表此条boundary可以使用。 接下来循环匹配where(payload中的where值),如果存在这样的where,设置whereMatch = True。如果clause和where中的一个没有匹配成功,都会结束循环,进入下一个payload的测试。

上面是设置payload的前缀和后缀,如果用户设置了,则使用用户设置的,如果没有,则使用boundary中的。
352行:

这里的where是payload中的where值,共有三个值,where字段我理解的意思是,以什么样的方式将我们的payload添加进去。

1:表示将我们的payload直接添加在值得后面[此处指的应该是检测的参数的值] 如我们写的参数是id=1,设置值为1的话,会出现1后面跟payload

2:表示将检测的参数的值更换为一个整数,然后将payload添加在这个整数的后面。 如我们写的参数是id=1,设置值为2的话,会出现[数字]后面跟payload

3:表示将检测的参数的值直接更换成我们的payload。 如我们写的参数是id=1,设置值为3的话,会出现值1直接被替换成了我们的payload。
最终在389行:

组合前缀、后缀、payload等,生成请求的reqPayload。
这其中有个cleanupPayload()函数,其实就是将一些值进行随机化。如下图,例如kb.chars.start,kb.chars.stop,这两个变量是在基于错误的注入时,随机产生的字符串。

enter image description here

在398行:

上面这部分代码非常多,通过for循环遍历payload中的标签,遍历的结果类似于

enter image description here

所以,上面的代码可以分为:

1.method为PAYLOAD.METHOD.COMPARISON:bool类型盲注 2.method为PAYLOAD.METHOD.GREP:基于错误的sql注入 3.mehtod为PAYLOAD.METHOD.TIME:基于时间的盲注 4.method为PAYLOAD.METHOD.UNION:union联合查询

请注意,上面这四种方法,和之前说的六种注入方法不是一个概念,这里的是payload中的response代码,而注入用的是request代码。通过比较request的结果和response的结果,确定是否可以注入。以后的文章会介绍怎么比较的。。
checkSqlInjectiond的关键部分就到这里了,后面就是把注入的数据保存起来。马上会介绍读取的时候。

3.2.4 Payload生成条件

前面具体介绍了Payload的生成方法,这里再总结一下条件:

1.sqlmap会实现读取payloads文件夹下xml文件中的每个test元素,然后循环遍历。

2.此时还会遍历boundaries.xml文件。

3.当且仅当某个boundary元素的where节点的值包含test元素where节点的值,clause节点的值包含test元素的clause节点的值,该boundary才能和当前的test匹配,从而进一步生成payload。

4.where字段有三个值1:表示将我们的payload直接添加在值得后面[此处指的应该是检测的参数的值] 如我们写的参数是id=1,设置值为1的话,会出现1后面跟payload 2:表示将检测的参数的值更换为一个整数,然后将payload添加在这个整数的后面。 如我们写的参数是id=1,设置值为2的话,会出现[数字]后面跟payload 3:表示将检测的参数的值直接更换成我们的payload。 如我们写的参数是id=1,设置值为3的话,会出现值1直接被替换成了我们的payload

5.最终的payload = url参数 + boundary.prefix+test.payload+boundary.suffix

3.2.5 Action

在start()的617行是action()函数,位于Action.py中,此函数是判断用户提供的参数,然后提供相应的函数。

3.2.6 HashDB

sqlmap注入的结果会保存在输出目录的session.sqlite文件汇总,此文件是sqlite数据库,可以使用SQLiteManager打开。
回到controller.py中的start函数。第602行

这四个函数的作用就是保存结果保存结果、保存session、显示注入结果,包括类型,payload等。
前面介绍过会判断testSqlInj的值,如果为True,代表没有测试过,会进入checkSqlInjection()函数,如果测试过,那么testSqlInj为false,就会跳过checkSqlInjection()。
比如我们选择–current-db时,通过action()进入到conf.dumper.currentDb(conf.dbmsHandler.getCurrentDb())。进入到databases.py的getCurrentDb中。

这是获取相应的命令,比如mysql的命令是database().一直跟踪函数到use.py的346行

_onehotUninoUse就是读取session文件,获取已经注入过的数据,如果session中没有,代表没有请求过,则重新请求获取数据。output此时是获取的网页的源码。

_onehotUninoUse的第一行,就是从session中获取数据,跟踪进hashdb.py的regrieve函数

通过HashDB.hashKey()计算id,然后到session.sqlite中找记录,那么key是怎么生成的呢?
在common.py中有个hashDBRetrieve(),

此函数用于生成hash的key,生成方法为url+’None’+命令+HASHDB_MILESTONE_VALUE,比如 u’http://127.0.0.1:80/biweb/archives /detail.phpNoneDATABASE()JHjrBugdDA’。此key经过 int(hashlib.md5(key).hexdigest()[:12], 16),就是对应session中的id

enter image description here

最终在session.sqlite中根据id,就能够找到记录。

enter image description here

如上图,获取到的记录其实就是一个网页的源代码,另外可以看到current-db的前后有几个字符串,这个字符串就是kb.chars.start和kb.chars.stop
回到_oneShotUnionUse中,如果session中没有记录,则会重新进行请求,获取数据

最终的值通过解析session中的记录value = parseUnionPage(output),找到kb.chars.start和kb.chars.stop中间的值,就是结果。

0x04 结束

还有很多东西没有写出来,希望后面的几篇文章能够写好。花了好久的时间,调试、码字,不知道又没有人能看到最后。。

[via@drops:3xpl0it]