Sqlmap Code Analysis

概述

不得不承认,Sqlmap是目前使用人数最多,功能最复杂的SQL注入工具。作为一款开源工具,开发者有意的让我们自行去阅读并对其进行扩充,从Github可以看出一直在更新的。对于渗透人员来说,阅读sqlmap源码是很有必要的,我们可以从它的源码之中学习到一些优秀的代码编写思维以及方式,应用到平时的渗透中去。

Sqlmap的结构

首先看看Sqlmap的目录结构:

sqlmap-dir

目录说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Doc 帮助文档
Extra 拓展程序,包含多种额外功能,例如运行cmd、安全执行、shellcode等
Lib 类库程序,包含多种连接库,如5种注入类型、提权操作
Plugins 插件程序,包括各种数据库的信息以及数据库的通用事项
Procs sql语句,包含oracle,postgresql 等的dns_request程序、Mysql的读写文件、mssql的xp_cmdshell等sql语句
Shell shell应用,包含多个语言注入成功的shell脚本
Tamper 多个绕过程序
Thirdparty 第三方程序,如beautifulsoup、优化,保持连接,颜色等等
Txt 爆破字典,如浏览器类型、 表,列字典
Udf 用户定义的mysql的攻击载荷
Waf waf防火墙特征指纹
Xml 多种数据库检测载荷以及指纹定义
sqlmap.conf 配置文件
sqlmap.py 主程序
sqlmap.api 接口程序

Sqlmap的流程图如下所示:

sqlmap-process

概括起来就是当在命令行输入命令的时候,sqlmap首先会进行初始化的操作(版本检测、模块检测、命令行参数解析、Session读取等),之后便会简单的检测是否存在注入点,如果存在注入点便会进行进一步的注入,最后才是接管(takeover)操作。

Sqlmap初始化

大部分工具在进行攻击时都会进行初始化,Sqlmap也不例外。为了保证Sqlmap的正常运行首先需要进行python的版本、功能模块的自检。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# sqlmap.py 开头,这里利用__import__()抛出的异常来检查必要库的存在。
try:
__import__("lib.utils.versioncheck") # this has to be the first non-standard import
except ImportError:
exit("[!] wrong installation detected (missing modules). Visit 'https://github.com/sqlmapproject/sqlmap/#installation' for further details")
```
下面是import的模块:
``` python
# /lib/utils/versioncheck.py
if PYVERSION >= "3" or PYVERSION < "2.6":
exit("[CRITICAL] incompatible Python version detected ('%s'). For successfully running sqlmap you'll have to use version 2.6.x or 2.7.x (visit 'http://www.python.org/download/')" % PYVERSION)
extensions = ("gzip", "ssl", "sqlite3", "zlib")
try:
for _ in extensions:
__import__(_)
except ImportError:
errMsg = "missing one or more core extensions (%s) " % (", ".join("'%s'" % _ for _ in extensions))
errMsg += "most likely because current version of Python has been "
errMsg += "built without appropriate dev packages (e.g. 'libsqlite3-dev')"
exit(errMsg)

在这之后才进入main()函数,而main()函数的开头便是检查环境(路径、版本)、输出banner信息、读取命令行参数。这里主要关注的是modulePath()函数,为了方便在Windows以及Linux上运行,这里统一将操作系统的编码转化为Unicode编码后返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# sqlmap.py
def main():
checkEnvironment()
setPaths(modulePath())
banner()
# Store original command line options for possible later restoration
cmdLineOptions.update(cmdLineParser().__dict__)
nitOptions(cmdLineOptions)
....
def modulePath():
...
return getUnicode(os.path.dirname(os.path.realpath(_)), encoding=sys.getfilesystemencoding() or UNICODE_ENCODING)

之后利用python的optionparser模块来对输入的参数进行分析,在此之前,提取出第一个参数前的内容,例python sqlmap.py作为参数传入OptionParser之后就是对参数进行分析了,首先是帮助、版本、log级有关参数。

1
2
3
4
5
6
7
8
9
# /lib/parse/cmdline
def cmdLineParser(argv=None):
...
try:
parser.add_option("--hh", dest="advancedHelp",action="store_true",help="Show advanced help message and exit")
parser.add_option("--version", dest="showVersion",action="store_true",help="Show program's version number and exit")
parser.add_option("-v", dest="verbose", type="int",help="Verbosity level: 0-6 (default %d)" % defaults.verbose)
....
...

主要显示如下:
sqlmap-help

接下来便是一个init()函数,千万不要小看这个init()函数,因为它做了很多很多的工作,当然它大部分与你输入的参数有关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# /lib/core/option.py
def init():
_useWizardInterface() # 引导界面、新手教学
setVerbosity() # 设置输出的详细程度
_saveConfig() # 保存参数到配置文件
_setRequestFromFile() # 读取文件中的http请求
_cleanupOptions() # 清除配置选项
_cleanupEnvironment() # 清除环境变量
_dirtyPatches() # 设置http的最大行数
_purgeOutput() # 文件粉碎
_checkDependencies() # 检查第三方库是否存在,比如mysql、mssql等连接库
_createTemporaryDirectory() # 创建临时目录
_basicOptionValidation() # 检查参数是否有效
_setProxyList() # 设置代理
_setTorProxySettings() # 设置tor代理
_setDNSServer() # 设置dns服务器
_adjustLoggingFormatter() # 调整日志格式
_setMultipleTargets() # 检测多个目标
_setTamperingFunctions() # tamper模块
_setWafFunctions() # waf识别模块
_setTrafficOutputFP()
_setupHTTPCollector()
_resolveCrossReferences()
_checkWebSocket() # 检测websocket
parseTargetUrl() # 分析目标检查url,并给config赋值
parseTargetDirect() # 分析目标数据库,并给config赋值
if any((conf.url, conf.logFile, conf.bulkFile, conf.sitemapUrl, conf.requestFile, conf.googleDork, conf.liveTest)):
_setHTTPTimeout()
_setHTTPExtraHeaders() # 设置http头
_setHTTPCookies()
_setHTTPReferer()
_setHTTPHost()
_setHTTPUserAgent()
_setHTTPAuthentication() # 设置http验证方式
_setHTTPHandlers()
_setDNSCache()
_setSocketPreConnect() # 创建一个预连接的socker connect
_setSafeVisit()
_doSearch() # 搜索url注入
_setBulkMultipleTargets() # 通过bulkfile读取多个url列表
_setSitemapTargets() # 分析sitemap读取url列表
_checkTor()
_setCrawler() # 页面爬取深度
_findPageForms() # 从页面中搜索表单
_setDBMS() # 设置数据库类型
_setTechnique() # 设置注入类型
_setThreads() # 设置线程数
_setOS()
_setWriteFile()
_setMetasploit() # 设置msf接管功能
_setDBMSAuthentication()
loadBoundaries() # 加载boundaries.xml文件
loadPayloads() # 加载payload.xml文件
_setPrefixSuffix()
update() # sqlmap自动更新
_loadQueries() # 加载queries.xml文件

在初始化之后,便根据不同的需求运行不同的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# sqlmap.py
#性能的自测功能,输出是图形文件,可以看出整个程序每个步骤的占用时间百分比、函数调用次数,便于能够直观看出程序运行的瓶颈所在。
if conf.profile:
profile()
# 冒烟测试,简单来说就是对一个软件镜像尽可能的功能覆盖测试。
elif conf.smokeTest:
smokeTest()
# 从livetests.xml里加载用于测试注入功能的网站和配置样例,进行全面的注入测试。
elif conf.liveTest:
liveTest()
else:
try:
start() # 真正的开始运行
except thread.error as ex:
....

前三个部分主要是软件的测试,和我们平时渗透没有多大关系,这里便不继续深究下去。

Sqlmap核心

数据库直连

sqlmap可以通过-d参数来直接连接目标数据库,不过一般需要额外安装第三方模块。

1
2
3
4
5
6
# /lib/controller/controller.py
if conf.direct:
initTargetEnv()
setupTargetEnv()
action()
return True

sqlmap-direct

读取Session

对一个网站进行sql注入测试的时候,sqlmap一般会在当前用户的目录下创建一个.sqlmap目录,里面一般会保存目标网站的一些日志、注入点设置、session信息。session信息一般会以sqllite数据库文件的形式存储,里面主要是id、value两个字段,具体图如下:

sqlmap-sqlite

它的value值一部分是以明文的形式存储,另一部是AttribDict这个对象序列化后的以base64编码后的形式存储的。

sqlmap-attribdict

注入标记符

接下来是检索注入标记符并给出提示询问是否对标记点检测注入,也就是我们常用的*,来指定注入的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# /lib/core/target.py
if conf.data is not None:
conf.method = HTTPMETHOD.POST if not conf.method or conf.method == HTTPMETHOD.GET else conf.method
hintNames = []
def process(match, repl):
retVal = match.group(0) # 先取出整个字符串
if not (conf.testParameter and match.group("name") not in conf.testParameter): # 如果没有指定注入参数
retVal = repl
while True:
_ = re.search(r"\\g<([^>]+)>", retVal)
if _:
retVal = retVal.replace(_.group(0), match.group(int(_.group(1)) if _.group(1).isdigit() else _.group(1)))
else:
break
if kb.customInjectionMark in retVal: # 如果有注入标记符
hintNames.append((retVal.split(kb.customInjectionMark)[0], match.group("name")))
return retVal
# 如果data中有注入标记符(这里默认的就是*星号,可以用来指定注入位置)
if kb.processUserMarks is None and kb.customInjectionMark in conf.data:
message = "custom injection marker ('%s') found in option " % kb.customInjectionMark
message += "'--data'. Do you want to process it? [Y/n/q] "
choice = readInput(message, default='Y').upper()
if choice == 'Q':
raise SqlmapUserQuitException
else:
kb.processUserMarks = choice == 'Y'
if kb.processUserMarks:
kb.testOnlyCustom = True

action

sqlmap 的aciton比较简介,功能也比较清楚,主要就是根据我们的参数来dump相应的数据以及接管的操作,我稍微列举了几个有代表性的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# /lib/controller/action.py
def action():
...
# dump
if conf.getDbs:
conf.dumper.dbs(conf.dbmsHandler.getDbs())
if conf.getTables:
conf.dumper.dbTables(conf.dbmsHandler.getTables())
if conf.commonTables:
conf.dumper.dbTables(tableExists(paths.COMMON_TABLES))
...
# takeover
if conf.osCmd:
conf.dbmsHandler.osCmd()
if conf.osShell:
conf.dbmsHandler.osShell()
if conf.regRead:
conf.dumper.registerValue(conf.dbmsHandler.regRead())
if conf.regAdd:
conf.dbmsHandler.regAdd()
....

Waf指纹

接下来是加载WAF/IDS/IPS测试函数(可选参数),conf.identifyWaf对应的是--identify-waf参数,sqlmap能够测试的WAF基本上是很齐全了,都在waf目录中。

sqlmap-waf

从上图中的右边代码可以清楚的看出,主要就是发送几个payload来查看服务器做出的回应。然后根据正则等方式进行waf的指纹匹配.

主要发送的payload我也看一下,由以下四部分组成:

1
2
3
4
5
6
7
8
9
10
# /lib/core/settings.py
WAF_ATTACK_VECTORS = (
"", # NIL
"search=<script>alert(1)</script>",
"file=../../../../etc/passwd",
"q=<invalid>foobar",
"id=1 %s" % IDS_WAF_CHECK_PAYLOAD
)
IDS_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')#"

crawl爬虫

sqlmap的爬虫模块主要--crawl这个参数有关,可以收集潜在的可能存在漏洞的连接,后面跟的参数是爬行的深度。crawl函数在爬虫模块/lib/utils/crawler.py中。代码就不进行列举了,简单的说明下就是Sqlmap会创建一个visited队列和一个value队列,然后进行爬行,先将页面的url通过正则、sitemap之后放入value队列(去重),然后将爬过了url放入visited队列(去重),每次爬行时都会先看看是否已经visited。

接下来是--forms,解析出页面的所有表单的功能实现。调用了/lib/core/common.py中的findPageForms()函数,而对于除了-u方式直接输入目标url的其他输入方式都采用先解析urls,再分别查表的方式

payload(核心)

Sqlmap最强大的就是它的注射技术,这也是它最为核心的部分。
Sqlmap启动后首先设置测试的level和risk,并识别受测试元素的类型,最终拼凑出相应的漏洞利用方案。
这里的payload它主要由boundariespayloadsqueries这几部分组成,且都保存在xml目录下。

sqlmap-xml

boundaries

boundaries.xml中保存了注入语句的前缀、后缀,主要就是用来闭合注入点处的前半部分和后半部分。举个例子来说: ?keyword=hello world这里keyword存在注入点,并且是搜索行注入,那么它在服务器端拼接成的语句就是:

1
select * from articles where title like '%hello world%';

所以sqlmap就需要闭合掉前面的引号和后面的引号,就可能会用到boundaries.xml的下列部分:

1
2
3
4
5
6
7
8
<boundary>
<level>1</level> <!-- \-\-level 的等级-->
<clause>1</clause> <!-- 从句的类型-->
<where>1,2</where> <!-- 语句注入处, 1 为原始数据后注入,2为随机数后注入-->
<ptype>2</ptype>
<prefix>%'</prefix> <!-- 闭合前半部分-->
<suffix> AND '%'='</suffix> <!-- 闭合后半部分-->
</boundary>

最终会闭合成:

1
select * from articles where title like '%hello world%' AND '%'='%';

payloads

xml/payloads 目标下保存着六种注入类型(基于布尔、基于时间、基于错误、union、内联、堆叠)的payload,一般以<test>为结点,<test>具有特定漏洞的全部信息,包括漏洞的level、risk,漏洞类型,利用的方法,检测方法等。主要格式如下:

sqlmap-payloads

1
(keyword='%hello world)+prefix(%')+payloads(AND [RANDNUM]=[RANDNUM])+comment(#)+suffix(AND ('%'=') + ( %')

queries

queries.xml主要就是具体的dump语句了:

1
2
3
4
5
6
7
8
9
10
<root>
<!-- MySQL -->
<dbms value="MySQL">
<!-- .... -->
<hostname query="@@HOSTNAME"/>
<table_comment query="SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='%s' AND table_name='%s'"/>
<column_comment query="SELECT column_comment FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema='%s' AND table_name='%s' AND column_name='%s'"/>
<is_dba query="(SELECT super_priv FROM mysql.user WHERE user='%s' LIMIT 0,1)='Y'"/>
<check_udf query="(SELECT name FROM mysql.func WHERE name='%s' LIMIT 0,1)='%s'"/>
<!-- .... -->

example

在测试一个参数的时候sqlmap会遍历所有符合要求的test节点,会分别发送request与response节点下的payload,然后对服务器响应的数据包进行对比,看其是否存在差异,当确认存在注入的时候,就会以vector来加载queries.xml中的查询语句进行注入。

拿个实际注入的例子来说:

sqlmap-payloads-1

sqlmap-payloads-2

sqlmap-payloads-3

Tamper

Sqlmap 的Tamper模块主要就是用来绕waf用的,一般都保存在tamper 目录中,可以依据标准格式自定义,比如把payload用like替换等号,base64编码等…..,tamper都是具有一定的格式的,如果在渗透测试中发现sqlmap自带的tamper不符合,便可以依样画葫芦写个tamper出来:

下面是sqlmap自带的tamper,主要将等号替换成like的tamper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# quealtolike.py
def dependencies():
singleTimeWarnMessage("tamper script '%s' is unlikely to work against %s" % (os.path.basename(__file__).split(".")[0], DBMS.PGSQL))
def tamper(payload, **kwargs):
"""
Replaces all occurances of operator equal ('=') with operator 'LIKE'
Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
Notes:
* Useful to bypass weak and bespoke web application firewalls that
filter the equal character ('=')
* The LIKE operator is SQL standard. Hence, this tamper script
should work against all (?) databases
>>> tamper('SELECT * FROM users WHERE id=1')
'SELECT * FROM users WHERE id LIKE 1'
"""
retVal = payload
if payload:
retVal = re.sub(r"\s*=\s*", " LIKE ", retVal)
return retVal

Sqlmap接管

接下来是跟提权/后门/系统有关的模块,主要与takeover.py这类型的脚本有关。

sqlmap-takeover

下面是osCmd() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def osCmd(self):
if isStackingAvailable() or conf.direct:
web = False
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
infoMsg = "going to use a web backdoor for command execution"
logger.info(infoMsg)
web = True
else:
errMsg = "unable to execute operating system commands via "
errMsg += "the back-end DBMS"
raise SqlmapNotVulnerableException(errMsg)
self.getRemoteTempPath() # 得到远程临时路径,写入webshell
self.initEnv(web=web)
if not web or (web and self.webBackdoorUrl is not None):
self.runCmd(conf.osCmd) # 执行命令
if not conf.osShell and not conf.osPwn and not conf.cleanup:
self.cleanup(web=web) # 清除shell

webshell

sqlmap自带了一部分的webshell,主要保存在shell目录下,由stager(上传马)、backdoor(执行马),不过这些webshell被压缩过了:

sqlmap-webshell

php的上传马(stager):

sqlmap-webshell-php-upload

sqlmap-webshell-php-upload-2

php的执行马(backdoor):

sqlmap-webshell-php-exec

各位有兴趣的可以改成自己的webshell,压缩的代码如下:

1
2
3
4
5
6
7
8
9
10
11
data = zlib.decompress(hideAscii(data))
def hideAscii(data):
retVal = ""
for i in xrange(len(data)):
if ord(data[i]) < 128:
retVal += chr(ord(data[i]) ^ 127)
else:
retVal += data[i]
return retVal

Sqlmap其他模块

文件粉碎

sqlmap自带了文件粉碎功能,可以安全删除文件,且无法恢复。主要由整理硬盘垃圾数据,获取文件句柄,填充垃圾数据,随机命名,最终删除这几个步骤组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def purge(directory):
...
# 遍历文件
for rootpath, directories, filenames in os.walk(directory):
dirpaths.extend([os.path.abspath(os.path.join(rootpath, _)) for _ in directories])
filepaths.extend([os.path.abspath(os.path.join(rootpath, _)) for _ in filenames])
# 修改文件读写属性
for filepath in filepaths:
try:
os.chmod(filepath, stat.S_IREAD | stat.S_IWRITE)
except:
pass
# 填充垃圾数据
for filepath in filepaths:
try:
filesize = os.path.getsize(filepath)
with open(filepath, "w+b") as f:
f.write("".join(chr(random.randint(0, 255)) for _ in xrange(filesize)))
except:
pass
# 截断文件 ...
# 重命名文件 ...
# 删除文件 ...

Sqlmap api

如果我们要做二次开发,就可以直接利用这个sqlmap的api,通过进程通信的方式来完成sqlmap的功能,具体请参考这里

sqlmap-api

异常汇报

在脚本运行中难免会碰到这样那样的错误,sqlmap因此做了很多异常捕获以及处理,如果sqlmap捕获到的异常它无法识别以及处理,便会上传到github上,因此来告知开发者对其进行bug的修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def createGithubIssue(errMsg, excMsg):
......
if choice:
ex = None
errMsg = errMsg[errMsg.find("\n"):]
req = urllib2.Request(url="https://api.github.com/search/issues?q=%s" % urllib.quote("repo:sqlmapproject/sqlmap Unhandled exception (#%s)" % key))
try:
content = urllib2.urlopen(req).read()
_ = json.loads(content)
duplicate = _["total_count"] > 0
closed = duplicate and _["items"][0]["state"] == "closed"
if duplicate:
warnMsg = "issue seems to be already reported"
if closed:
warnMsg += " and resolved. Please update to the latest "
warnMsg += "development version from official GitHub repository at '%s'" % GIT_PAGE
logger.warn(warnMsg)
return
except:
pass

自动更新

这部分就不便多展开了,主要就是指sqlmap支持一键升级的功能:

sqlmap-update

总结

这几天大致阅读了Sqlmap的源码,才明白了Sqlmap的强大之处不仅仅在于它的注射功能,更在于它那优秀的代码编码方式、优秀的设计理念、Pythonic 的技巧、各种Python内置模块以及第三方模块的调用等等。也因此学习到了很多新的知识,比如临时文件的粉碎级别删除等等,看了它那优秀的编码后才明白自己平时渗透测试时写的脚本看上去狗爬一般。看来平时还得多抽空看看那些有些的渗透框架、工具,这样才可以尽量避免闭门造车,省去很多很多精力。