sqlmap源码通读 1.0

Sqlmap源码通读 1.0

花了一个下午的时间看了一下SQLmap用户手册,发现以前对SQLmap的认知真是太肤浅了。

1.0主要还是看Sqlmap的基础功能和实现细节

2.0打算看看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
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
用法:python sqlmap.py [选项]

选项:
-h, --help 显示基本帮助信息并退出
-hh 显示高级帮助信息并退出
--version 显示程序版本信息并退出
-v VERBOSE 输出信息详细程度级别:0-6(默认为 1)

目标:
至少提供一个以下选项以指定目标

-d DIRECT 直接连接数据库
-u URL, --url=URL 目标 URL(例如:"http://www.site.com/vuln.php?id=1")
-l LOGFILE 从 Burp 或 WebScarab 代理的日志文件中解析目标地址
-m BULKFILE 从文本文件中获取批量目标
-r REQUESTFILE 从文件中读取 HTTP 请求
-g GOOGLEDORK 使用 Google dork 结果作为目标
-c CONFIGFILE 从 INI 配置文件中加载选项

请求:
以下选项可以指定连接目标地址的方式

--method=METHOD 强制使用提供的 HTTP 方法(例如:PUT)
--data=DATA 使用 POST 发送数据串(例如:"id=1")
--param-del=PARA.. 设置参数值分隔符(例如:&)
--cookie=COOKIE 指定 HTTP Cookie(例如:"PHPSESSID=a8d127e..")
--cookie-del=COO.. 设置 cookie 分隔符(例如:;)
--load-cookies=L.. 指定以 Netscape/wget 格式存放 cookies 的文件
--drop-set-cookie 忽略 HTTP 响应中的 Set-Cookie 参数
--user-agent=AGENT 指定 HTTP User-Agent
--random-agent 使用随机的 HTTP User-Agent
--host=HOST 指定 HTTP Host
--referer=REFERER 指定 HTTP Referer
-H HEADER, --hea.. 设置额外的 HTTP 头参数(例如:"X-Forwarded-For: 127.0.0.1")
--headers=HEADERS 设置额外的 HTTP 头参数(例如:"Accept-Language: fr\nETag: 123")
--auth-type=AUTH.. HTTP 认证方式(Basic,Digest,NTLM 或 PKI)
--auth-cred=AUTH.. HTTP 认证凭证(username:password)
--auth-file=AUTH.. HTTP 认证 PEM 证书/私钥文件
--ignore-code=IG.. 忽略(有问题的)HTTP 错误码(例如:401)
--ignore-proxy 忽略系统默认代理设置
--ignore-redirects 忽略重定向尝试
--ignore-timeouts 忽略连接超时
--proxy=PROXY 使用代理连接目标 URL
--proxy-cred=PRO.. 使用代理进行认证(username:password)
--proxy-file=PRO.. 从文件中加载代理列表
--tor 使用 Tor 匿名网络
--tor-port=TORPORT 设置 Tor 代理端口代替默认端口
--tor-type=TORTYPE 设置 Tor 代理方式(HTTP,SOCKS4 或 SOCKS5(默认))
--check-tor 检查是否正确使用了 Tor
--delay=DELAY 设置每个 HTTP 请求的延迟秒数
--timeout=TIMEOUT 设置连接响应的有效秒数(默认为 30)
--retries=RETRIES 连接超时时重试次数(默认为 3)
--randomize=RPARAM 随机更改给定的参数值
--safe-url=SAFEURL 测试过程中可频繁访问且合法的 URL 地址(译者注:
有些网站在你连续多次访问错误地址时会关闭会话连接,
后面的“请求”小节有详细说明)
--safe-post=SAFE.. 使用 POST 方法发送合法的数据
--safe-req=SAFER.. 从文件中加载合法的 HTTP 请求
--safe-freq=SAFE.. 每访问两次给定的合法 URL 才发送一次测试请求
--skip-urlencode 不对 payload 数据进行 URL 编码
--csrf-token=CSR.. 设置网站用来反 CSRF 攻击的 token
--csrf-url=CSRFURL 指定可提取防 CSRF 攻击 token 的 URL
--force-ssl 强制使用 SSL/HTTPS
--hpp 使用 HTTP 参数污染攻击
--eval=EVALCODE 在发起请求前执行给定的 Python 代码(例如:
"import hashlib;id2=hashlib.md5(id).hexdigest()")

优化:
以下选项用于优化 sqlmap 性能

-o 开启所有优化开关
--predict-output 预测常用请求的输出
--keep-alive 使用持久的 HTTP(S) 连接
--null-connection 仅获取页面大小而非实际的 HTTP 响应
--threads=THREADS 设置 HTTP(S) 请求并发数最大值(默认为 1)

注入:
以下选项用于指定要测试的参数,
提供自定义注入 payloads 和篡改参数的脚本

-p TESTPARAMETER 指定需要测试的参数
--skip=SKIP 指定要跳过的参数
--skip-static 指定跳过非动态参数
--param-exclude=.. 用正则表达式排除参数(例如:"ses")
--dbms=DBMS 指定后端 DBMS(Database Management System,
数据库管理系统)类型(例如:MySQL)
--dbms-cred=DBMS.. DBMS 认证凭据(username:password)
--os=OS 指定后端 DBMS 的操作系统类型
--invalid-bignum 将无效值设置为大数
--invalid-logical 对无效值使用逻辑运算
--invalid-string 对无效值使用随机字符串
--no-cast 关闭 payload 构造机制
--no-escape 关闭字符串转义机制
--prefix=PREFIX 注入 payload 的前缀字符串
--suffix=SUFFIX 注入 payload 的后缀字符串
--tamper=TAMPER 用给定脚本修改注入数据

检测:
以下选项用于自定义检测方式

--level=LEVEL 设置测试等级(1-5,默认为 1)
--risk=RISK 设置测试风险等级(1-3,默认为 1)
--string=STRING 用于确定查询结果为真时的字符串
--not-string=NOT.. 用于确定查询结果为假时的字符串
--regexp=REGEXP 用于确定查询结果为真时的正则表达式
--code=CODE 用于确定查询结果为真时的 HTTP 状态码
--text-only 只根据页面文本内容对比页面
--titles 只根据页面标题对比页面

技术:
以下选项用于调整特定 SQL 注入技术的测试方法

--technique=TECH 使用的 SQL 注入技术(默认为“BEUSTQ”,译者注:
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 injection(内联查询注入)
--time-sec=TIMESEC 延迟 DBMS 的响应秒数(默认为 5)
--union-cols=UCOLS 设置联合查询注入测试的列数目范围
--union-char=UCHAR 用于暴力猜解列数的字符
--union-from=UFROM 设置联合查询注入 FROM 处用到的表
--dns-domain=DNS.. 设置用于 DNS 渗出攻击的域名(译者注:
推荐阅读《在SQL注入中使用DNS获取数据》
http://cb.drops.wiki/drops/tips-5283.html,
在后面的“技术”小节中也有相应解释)
--second-url=SEC.. 设置二阶响应的结果显示页面的 URL(译者注:
该选项用于 SQL 二阶注入)
--second-req=SEC.. 从文件读取 HTTP 二阶请求

指纹识别:
-f, --fingerprint 执行广泛的 DBMS 版本指纹识别

枚举:
以下选项用于获取后端 DBMS 的信息,结构和数据表中的数据。
此外,还可以运行你输入的 SQL 语句

-a, --all 获取所有信息、数据
-b, --banner 获取 DBMS banner
--current-user 获取 DBMS 当前用户
--current-db 获取 DBMS 当前数据库
--hostname 获取 DBMS 服务器的主机名
--is-dba 探测 DBMS 当前用户是否为 DBA(数据库管理员)
--users 枚举出 DBMS 所有用户
--passwords 枚举出 DBMS 所有用户的密码哈希
--privileges 枚举出 DBMS 所有用户特权级
--roles 枚举出 DBMS 所有用户角色
--dbs 枚举出 DBMS 所有数据库
--tables 枚举出 DBMS 数据库中的所有表
--columns 枚举出 DBMS 表中的所有列
--schema 枚举出 DBMS 所有模式
--count 获取数据表数目
--dump 导出 DBMS 数据库表项
--dump-all 导出所有 DBMS 数据库表项
--search 搜索列,表和/或数据库名
--comments 枚举数据时检查 DBMS 注释
-D DB 指定要枚举的 DBMS 数据库
-T TBL 指定要枚举的 DBMS 数据表
-C COL 指定要枚举的 DBMS 数据列
-X EXCLUDE 指定不枚举的 DBMS 标识符
-U USER 指定枚举的 DBMS 用户
--exclude-sysdbs 枚举所有数据表时,指定排除特定系统数据库
--pivot-column=P.. 指定主列
--where=DUMPWHERE 在转储表时使用 WHERE 条件语句
--start=LIMITSTART 指定要导出的数据表条目开始行数
--stop=LIMITSTOP 指定要导出的数据表条目结束行数
--first=FIRSTCHAR 指定获取返回查询结果的开始字符位
--last=LASTCHAR 指定获取返回查询结果的结束字符位
--sql-query=QUERY 指定要执行的 SQL 语句
--sql-shell 调出交互式 SQL shell
--sql-file=SQLFILE 执行文件中的 SQL 语句

暴力破解:
以下选项用于暴力破解测试

--common-tables 检测常见的表名是否存在
--common-columns 检测常用的列名是否存在

用户自定义函数注入:
以下选项用于创建用户自定义函数

--udf-inject 注入用户自定义函数
--shared-lib=SHLIB 共享库的本地路径

访问文件系统:
以下选项用于访问后端 DBMS 的底层文件系统

--file-read=FILE.. 读取后端 DBMS 文件系统中的文件
--file-write=FIL.. 写入到后端 DBMS 文件系统中的文件
--file-dest=FILE.. 使用绝对路径写入到后端 DBMS 中的文件

访问操作系统:
以下选项用于访问后端 DBMS 的底层操作系统

--os-cmd=OSCMD 执行操作系统命令
--os-shell 调出交互式操作系统 shell
--os-pwn 调出 OOB shell,Meterpreter 或 VNC
--os-smbrelay 一键调出 OOB shell,Meterpreter 或 VNC
--os-bof 利用存储过程的缓冲区溢出
--priv-esc 数据库进程用户提权
--msf-path=MSFPATH Metasploit 框架的本地安装路径
--tmp-path=TMPPATH 远程临时文件目录的绝对路径

访问 Windows 注册表:
以下选项用于访问后端 DBMS 的 Windows 注册表

--reg-read 读取一个 Windows 注册表键值
--reg-add 写入一个 Windows 注册表键值数据
--reg-del 删除一个 Windows 注册表键值
--reg-key=REGKEY 指定 Windows 注册表键
--reg-value=REGVAL 指定 Windows 注册表键值
--reg-data=REGDATA 指定 Windows 注册表键值数据
--reg-type=REGTYPE 指定 Windows 注册表键值类型

通用选项:
以下选项用于设置通用的参数

-s SESSIONFILE 从文件(.sqlite)中读入会话信息
-t TRAFFICFILE 保存所有 HTTP 流量记录到指定文本文件
--batch 从不询问用户输入,使用默认配置
--binary-fields=.. 具有二进制值的结果字段(例如:"digest")
--check-internet 在访问目标之前检查是否正常连接互联网
--crawl=CRAWLDEPTH 从目标 URL 开始爬取网站
--crawl-exclude=.. 用正则表达式筛选爬取的页面(例如:"logout")
--csv-del=CSVDEL 指定输出到 CVS 文件时使用的分隔符(默认为“,”)
--charset=CHARSET 指定 SQL 盲注字符集(例如:"0123456789abcdef")
--dump-format=DU.. 导出数据的格式(CSV(默认),HTML 或 SQLITE)
--encoding=ENCOD.. 指定获取数据时使用的字符编码(例如:GBK)
--eta 显示每个结果输出的预计到达时间
--flush-session 清空当前目标的会话文件
--forms 解析并测试目标 URL 的表单
--fresh-queries 忽略存储在会话文件中的查询结果
--har=HARFILE 将所有 HTTP 流量记录到一个 HAR 文件中
--hex 获取数据时使用 hex 转换
--output-dir=OUT.. 自定义输出目录路径
--parse-errors 从响应中解析并显示 DBMS 错误信息
--preprocess=PRE.. 使用给定脚本预处理响应数据
--repair 重新导出具有未知字符的数据(?)
--save=SAVECONFIG 将选项设置保存到一个 INI 配置文件
--scope=SCOPE 用正则表达式从提供的代理日志中过滤目标
--test-filter=TE.. 根据 payloads 和/或标题(例如:ROW)选择测试
--test-skip=TEST.. 根据 payloads 和/或标题(例如:BENCHMARK)跳过部分测试
--update 更新 sqlmap

杂项:
-z MNEMONICS 使用短助记符(例如:“flu,bat,ban,tec=EU”)
--alert=ALERT 在找到 SQL 注入时运行 OS 命令
--answers=ANSWERS 设置预定义回答(例如:“quit=N,follow=N”)
--beep 出现问题提醒或在发现 SQL 注入时发出提示音
--cleanup 指定移除 DBMS 中的特定的 UDF 或者数据表
--dependencies 检查 sqlmap 缺少(可选)的依赖
--disable-coloring 关闭彩色控制台输出
--gpage=GOOGLEPAGE 指定页码使用 Google dork 结果
--identify-waf 针对 WAF/IPS 防护进行彻底的测试
--mobile 使用 HTTP User-Agent 模仿智能手机
--offline 在离线模式下工作(仅使用会话数据)
--purge 安全删除 sqlmap data 目录所有内容
--skip-waf 跳过启发式检测 WAF/IPS 防护
--smart 只有在使用启发式检测时才进行彻底的测试
--sqlmap-shell 调出交互式 sqlmap shell
--tmp-dir=TMPDIR 指定用于存储临时文件的本地目录
--web-root=WEBROOT 指定 Web 服务器根目录(例如:"/var/www")
--wizard 适合初级用户的向导界面

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── data //数据库注入检测荷载、用户自定义攻击荷载、字典、shell命令、数据库触发顺序
├── doc
├── extra //额外功能:运行cmd、shellcode等
├── lib //Sqlmap的连接库,注入请求的参数、提权操作等
├── LICENSE
├── plugins //各种数据库信息和数据库通用事项
├── README.md
├── sqlmapapi.py //sqlmap的api文件
├── sqlmapapi.yaml //sqlmap的api文档
├── sqlmap.conf //sqlmap配置文件
├── sqlmap.py //sqlmap主程序文件
├── tamper //绕过脚本
└── thirdparty //第三方插件

函数

Sqlmap版本:1.5.11.9#dev

ps:刚开始看的时候用的VScode,后面换了Pycharm,前后截图看着难受的见谅= =

sqlmap.py

程序初始化以后,main()函数首先执行了五个函数

dirtyPatches()

对于程序中一些问题的处理和修复

image-20211120191640212

  • 设置httplib最大长度来接收长结果行
  • 在sqlmap分块的情况下防止双分块编码
  • 在 Windows 操作系统上添加对 inet_pton() 的支持(inet_pton()为IP地址转换函数),然后编码替换把cp65001替换为UTF-8
  • 在二进制数据检索的情况下防止过多的“guessing”

对sqlmap的功能影响似乎不是很大,可能是为了优化体验和进行一些基本设置

resolveCrossReferences()

解决模块的交叉引用问题

image-20211120194950636

对一些子程序中的函数进行重写赋值来消除交叉引用问题

checkEnvironment()

环境检测函数

image-20211120195101916

调用**modulePath()**获取程序路径,判断程序是否被py2exe(py2exe是一个将python脚本转换成windows上的可独立执行的可执行程序的工具)打包成了exe,打包后将无法用file获取文件路径。

image-20211120201307980

image-20211120201332414

用**getUnicode()**返回unicode编码路径,防止乱码。

用**LooseVersion()**判断python版本,python版本过低则报错退出

导入对pip安装环境的补丁,设置了一些系统环境变量

setPaths()

配置Sqlmap文件和目录的绝对路径

image-20211123184314072

判断扩展名为”.txt”, “.xml”, “.tx_”的文件是否存在并且可读

image-20211123184334026

image-20211123184419708

paths的值为Sqlmap自定义的一个字典类型AttribDict(这块不是很懂)

1
2
# sqlmap paths
paths = AttribDict()

This class defines the dictionary with added capability to access members as attributes

此类定义了具有访问成员作为属性的附加功能的字典

就是说可以通过访问属性的方式来访问键值

image-20211123185342627

其中定义了__deepcopy__,为了解决字典赋值传递后浅拷贝会修改原数据的问题

  • 直接赋值:其实就是对象的引用(别名)。
  • 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。
  • 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。

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

image-20211123192427780

BANNER就是Sqlmap开始运行时打印出来的字符画

image-20211123192506396

五个函数执行之后的流程

接下来是对命令行参数的操作

image-20211123195358545

cmdLineParse():该函数解析命令行参数和实参。使用了argparse — 命令行选项、参数和子命令解析器。将获取的命令行参数选项进行判断、拆分转变成dict键值对的形式存入cmdLineOptions(为一个AttribDict())。

接着cmdLineOptions传入initOptions()

image-20211123200106597

1
2
3
_setConfAttributes()  //初始化某些属性值为空
_setKnowledgeBaseAttributes() //初始化某些属性值到"知识库"中
_mergeOptions(inputOptions, overrideOptions) //将配置项中的参数和命令行获得的参数选项以及缺省选项进行合并,函数执行完毕将会将字典 mergedOptions 的值进行更新

判断是否标准输入

image-20211123200913831

image-20211123201300160

判断是否调用api,如果是将会引入几个新的包,并覆盖系统标准输出和标准错误

image-20211123201548376

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

image-20211123201709718

接着执行init()函数,该函数定义在lib/core/option.py

注释写的是:将属性设置为配置和知识库单例,基于命令行和配置文件选项。具体分析于下文。

之后执行两个测试函数smokeTest、vulnTest。不测试则导入包,判断conf.profile,其中**profile()**的作用写的是:以图形显示分析数据,不是很理解到底干了啥

image-20211124151442667

conf.profile默认为空,直接到else部分

image-20211124152338670

if判断中

#Crawl the website starting from the target URL.

conf.crawlDepth = 0

# Scan multiple targets enlisted in a given textual file

bulkFile =

首次运行if判断条件不成立,直接执行start(),start()单独分析。

接下来一堆except就不分析了。

finally中主要是给出一些提示信息(例如:"your sqlmap version is outdated")、清除临时目录、清除线程、清除配置等文件。

在最后判断命令行参数中是否存在sqlmapShell,如果有清除一波以后再次执行**main()**。

image-20211124154720290

option.py

命令行参数处理

init()

1
2
3
注释Google翻译:
--将属性设置为配置和知识库单例
--基于命令行和配置文件选项。
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
60
61
62
63
64
65
def init():
"""
Set attributes into both configuration and knowledge base singletons
based upon command line and configuration file options.
"""

_useWizardInterface() #为初学者提供简单的向导界面 开头用法中的 --wizard
setVerbosity() #此函数设置 sqlmap 输出消息的详细程度 详见:置顶手册->用法->输出详细等级
_saveConfig() #将命令行选项保存为sqlmap配置ini文件格式
_setRequestFromFile() #从文件中设置http请求 -r
_cleanupOptions() #清除配置选项
_cleanupEnvironment() #清理环境(例如,--shell后的残留)
_purge() #安全删除(清除)sqlmap 数据目录
_checkDependencies() #检测丢失的依赖
_createHomeDirectories() #在sqlmap的主目录中创建目录
_createTemporaryDirectory() #为这次运行创建临时目录
_basicOptionValidation() #检查选项是否有效
_setProxyList() #设置代理列表 --proxy
_setTorProxySettings() //设置Tor代理 --tor
_setDNSServer() #设置DNS服务器 --dns-domain
_adjustLoggingFormatter() #用于解决推理模式下日志信息和检索数据信息重叠导致的行删除问题
_setMultipleTargets() #多目标模式下运行,只定义一个配置参数
_listTamperingFunctions() #列出可用的Tamper功能
_setTamperingFunctions() #设置Tamper脚本 --temper
_setPreprocessFunctions() #从给定脚本加载预处理功能 --preprocess
_setPostprocessFunctions() #从给定脚本加载后处理功能
_setTrafficOutputFP() #设置记录http日志
_setupHTTPCollector() #设置http收集器
_setHttpChunked() #设置http chunked编码
_checkWebSocket() #检测websocket-client模块调用
parseTargetDirect() #解析目标 dbms 并将一些属性设置到配置中

if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)):
#如果设置了上述内容则配置http
_setHostname() #设置hostname
_setHTTPTimeout() #设置超时
_setHTTPExtraHeaders() #设置headers
_setHTTPCookies() #设置Cookie
_setHTTPReferer() #设置Referer
_setHTTPHost() #设置Host
_setHTTPUserAgent() #设置UserAgent
_setHTTPAuthentication() #设置Authentication
_setHTTPHandlers() #检查并设置所有 HTTP 请求的 HTTP/SOCKS 代理
_setDNSCache() #设置DNSCache(具体作用可以百度一下,和缓存差不多)
_setSocketPreConnect() #创建socket.create_connection 的预连接
_setSafeVisit() #检查并设置安全访问选项
_doSearch() #使用搜索引擎搜索并存储结果
_setStdinPipeTargets() #使用标准输入解析目标列表
_setBulkMultipleTargets() #从指定的批量文件解析多个目标
_checkTor() #检查Tor代理
_setCrawler() #设置爬虫
_findPageForms() #查找页面表单
_setDBMS() #强制设置DBMS --dbms
_setTechnique() #设置注入技术 --technique 详见上文"用法"部分

_setThreads() #设置线程
_setOS() #强制设置系统
_setWriteFile() #写入文件
_setMetasploit() #设置Metasploit
_setDBMSAuthentication() #设置DBMS验证(身份认证)
loadBoundaries() #加载Boundaries
loadPayloads() #加载Payloads
_setPrefixSuffix()
update() #更新sqlmap
_loadQueries() #从 'xml/queries.xml' file.loadQueries 加载查询

controller.py

start()

该函数对URL稳定性,所有GET、POST、Cookie和User-Agent参数进行检查,检查它们是否动态且受到Sql注入影响

创建hashFile。

conf.direct = True

conf.direct 当参数存在 -d(直接连接数据库)时为True,否则将绕过直接走下面的步骤

image-20211127183902435

initTargetEnv()

初始化目标环境,完成全局变量 conf 和 kb 的初始化工作(是否有自定义注入点,是否指定数据库)

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
def initTargetEnv():
"""
Initialize target environment.
"""

if conf.multipleTargets:
if conf.hashDB:
conf.hashDB.close()

if conf.cj:
resetCookieJar(conf.cj)

threadData = getCurrentThreadData()
threadData.reset()

conf.paramDict = {}
conf.parameters = {}
conf.hashDBFile = None

_setKnowledgeBaseAttributes(False)
_restoreMergedOptions()
_setDBMS() #强制设置DBMS

if conf.data:
class _(six.text_type):
pass

kb.postUrlEncode = True

for key, value in conf.httpHeaders:
if key.upper() == HTTP_HEADER.CONTENT_TYPE.upper():
kb.postUrlEncode = "urlencoded" in value
break

if kb.postUrlEncode:
#如果有urlencode会进行urldecode操作
original = conf.data
conf.data = _(urldecode(conf.data))
setattr(conf.data, UNENCODED_ORIGINAL_VALUE, original)
kb.postSpaceToPlus = '+' in original

match = re.search(INJECT_HERE_REGEX, "%s %s %s" % (conf.url, conf.data, conf.httpHeaders))
kb.customInjectionMark = match.group(0) if match else CUSTOM_INJECTION_MARK_CHAR
#CUSTOM_INJECTION_MARK_CHAR = '*'自定义注入标记字符
#INJECT_HERE_REGEX = r"(?i)%INJECT[_ ]?HERE%"

setupTargetEnv()

设置目标环境

1
2
3
4
5
6
7
8
9
10
11
def setupTargetEnv():
_createTargetDirs() #创建输出目录
_setRequestParams()
#检查、设置参数GET参数,还有POST中的--data内容
#检测注入标记字符,是否测试自定义注入点
#接下来基于注入标记一堆交互操作进行测试
_setHashDB()
_resumeHashDBValues()
_setResultsFile() #创建用于存储多目标模式运行结果的文件
_setAuthCred() #将当前目标的身份验证凭据添加到密码管理器(由连接处理程序使用)
_setAuxOptions()

action()

函数用于在受影响的URL参数上执行SQL注入攻击,并尝试提取 DBMS 或 操作系统相关信息

conf.direct = False

在没有直连数据库的情况下,将配置文件中的url、method、data、cookie添加到kb.targets中,接着判断如果不存在目标则报错,存在目标则打印出目标数

image-20211127194659375

判断网络连接状况,打印错误信息

image-20211127200035159

设置Http连接参数

1
initTargetEnv() //详见上文parseTargetUrl() //解析url获取信息(hostname、scheme...)

image-20211127200336549

kb.testedParams用于保存测试过的url参数信息,通过这个来判断url是否进行过test并且是否为注入点等,再来对testSqlinj进行修改,最后判断是否要跳过这个点

image-20211127201921315

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

**setupTargetEnv()**上文有不继续讲了(可能讲的不对= =)

检查连接、是否有用户自定义的字符或者正则,即

1
CUSTOM_INJECTION_MARK_CHAR = '*' //自定义注入标记字符INJECT_HERE_REGEX = r"(?i)%INJECT[_ ]?HERE%" //正则

**checkWaf()**,详见下文分析

checkNullConnection()可以参考一下链接。大概就是在进行盲注时,不用获取整个页面响应主体内容,但能通过类似Content-Length header知道内容长度,以此来节省带宽的一种操作。

image-20211130203726347

checkStability()用于检查URL稳定性,两次请求同一页面并在两次访问中带有延迟来确保稳定,如果两次请求的内容有差异(动态页面)则通过–string来判断是否为同一页面,接着对参数列表和测试列表进行排序。

image-20211130204237981

根据用户选择的**–level**来判断是否进行cookie、referer、ua的注入以及参数的跳过

image-20211206140046839

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

如果参数为动态,进入SQL注入测试,调用**heuristcCheckSqlInjection()**函数进行启发式检测,函数分析见下文。

得到POSITIVE结果后调用checkSqlInjection()进行注入点检查,再次确定有漏洞并且非FALSE_POSTTIVE,会设置injectable 设为True(可注入),中间产的数据会放入数据集中

image-20211206140706106

没有检测出漏洞点的情况下,会根据情况提供建议参数

image-20211206140954358

否则讲结果进行保存

image-20211206141110043

确认有注入点后,会与用户进行交互确认,执行**action()**函数。

下面是except异常处理,不细说。

显示http错误代码,提示最大连接限制

image-20211206141446717

image-20211206141544427

checkWaf()

1
def checkWaf():    #如果使用了自定义的参数或者跳过检测会直接return None,跳过waf检测    if any((conf.string, conf.notString, conf.regexp, conf.dummy, conf.offline, conf.skipWaf)):        return None    #判断原始状态码,不存在则return None    if kb.originalCode == _http_client.NOT_FOUND:        return None    #取出之前测试存储的数据,判断之前测试的时候有没有waf,如果取出数据不为空,则与探测发现目标地址存在WAF/IPS设备防御    _ = hashDBRetrieve(HASHDB_KEYS.CHECK_WAF_RESULT, True)    if _ is not None:        if _:            warnMsg = "previous heuristics detected that the target "            warnMsg += "is protected by some kind of WAF/IPS"            logger.critical(warnMsg)        return _    #不存在原始界面    if not kb.originalPage:        return None    infoMsg = "checking if the target is protected by "    infoMsg += "some kind of WAF/IPS"    logger.info(infoMsg)

payload由随机数字和sqlmap提前设置好的payload来进行WAF检测

1
retVal = False    payload = "%d %s" % (randomInt(), IPS_WAF_CHECK_PAYLOAD)

IPS_WAF_CHECK_PAYLOAD位置:lib/cpre/settings.py

可以看到包括了xss、sql、命令执行等

1
# Payload used for checking of existence of WAF/IPS (dummier the better)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进行拼接,根据是否重定向进行参数配置。

1
if PLACE.URI in conf.parameters:        place = PLACE.POST        value = "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload))    else:        place = PLACE.GET        value = "" if not conf.parameters.get(PLACE.GET) else conf.parameters[PLACE.GET] + DEFAULT_GET_POST_DELIMITER        value += "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload))    pushValue(kb.choices.redirect)    pushValue(kb.resendPostOnRedirect)    pushValue(conf.timeout)    kb.choices.redirect = REDIRECTION.YES    kb.resendPostOnRedirect = False    conf.timeout = IPS_WAF_CHECK_TIMEOUT    try:        #queryPage()这个函数用于获取目标页面内容并返回页面比例(0<=ratio<=1)或者表示布尔值(0||1),如果这个值小于0.5(IPS_WAF_CHECK_RATIO)则返回True存在WAF,反之False则不存在WAF        retVal = (Request.queryPage(place=place, value=value, getRatioValue=True, noteResponseTime=False, silent=True, raise404=False, disableTampering=True)[1] or 0) < IPS_WAF_CHECK_RATIO    except SqlmapConnectionException:        retVal = True    finally:        kb.matchRatio = None

测试完毕,将结果写入数据库,并根据结果进行用户交互。

目标可能存在某种WAF/IPS,是否进行更深度的测试。如果没有**–tamper**会让你考虑使用tamper脚本绕过WAF。

image-20211130193414009

identYwaf

Sqlmap有个插件用于识别WAF指纹,该插件位于:**/thirdparty/identywaf/identYwaf.py**

通过全局搜索可以发现在**/lib/request/basic.py** 中的processResponse()函数引用该插件,processResponse()又在lib/request/connect.py中的**getPage(),而getPage()的调用场景就比较多了,在Sqlmap需要获取页面信息的时候就会调用这个函数(还记得checkWaf()**中有一段代码比较返回页面内容比例么?),有点绕梳理一下过程。

1
getPage()->processResponse()->identYwaf.py

来简单看一下processResponse()

**parseResponse()**函数根据web应用返回的DBMS错误信息中后端DBMS指纹来检测判断可能的后台数据库。

image-20211207160143521

接着if比较了kb.processResponseCounterIDENTYWAF_PARSE_LIMIT,前者每次调用**processResponse()**加1,后者默认为10。可以看到这里会打印出WAF/IPS的识别结果

image-20211207160312902

具体看下non_blind_check(),这里进行了一个正则匹配,匹配WAF_RECOGNITION_REGEXrawResponse或””中的内容,将匹配到的结果加入non_blind中。

WAF_RECOGNITION_REGEXidentYwafA.py中的一个全局变量,通过load_data()data.json导入

1
def non_blind_check(raw, silent=False):    retval = False    match = re.search(WAF_RECOGNITION_REGEX, raw or "")    if match:        retval = True        for _ in match.groupdict():            if match.group(_):                waf = re.sub(r"\Awaf_", "", _)                non_blind.add(waf)                if not silent:                    single_print(colorize("[+] non-blind match: '%s'%s" % (format_name(waf), 20 * ' ')))    return retval

下面是data.json中关于waf的部分截图

image-20211207161722271

Sqlmap通过Payload检测目标是否有WAF,processResponse会跑十次,通过十次连接返回的结果并进行正则匹配来识别WAF指纹信息

heuristcCheckSqlInjection()

启发式注入,用各种payload进行测试

1
check = heuristicCheckSqlInjection(place, parameter)

开始从conf中拿数据,包括是否跳过测试

image-20211207191644488

自定义前后缀,下面举个例子

prefix: ‘)’

suffix: ‘AND ([RANDNUM]=[RANDNUM]’

假设的完整payload:1) AND 7862=7862 AND (9976=9976

image-20211207193008973

生成随机字符串,例如'),)\'.",(.)',其中

1
# Alphabet used for heuristic checksHEURISTIC_CHECK_ALPHABET = ('"', '\'', ')', '(', ',', '.')

image-20211207193110200

对payload进行拼接,并用**queryPage()**比对页面相似度

image-20211207193436099

接下来两个函数parseFilePaths()wasLastResponseDBMSError()

image-20211207194325930

1
parseFilePaths(page) #检测页面中(可能的)系统绝对路径wasLastResponseDBMSError() #检测是否出现(可识别的)数据库报错

检测服务端是否存在格式化参数出现报错信息的情况

image-20211207195446474

1
# Strings for detecting formatting errorsFORMAT_EXCEPTION_STRINGS = ("Type mismatch", "Error converting", "Please enter a", "Conversion failed", "String or binary data would be truncated", "Failed to convert", "unable to interpret text value", "Input string was not in a correct format", "System.FormatException", "java.lang.NumberFormatException", "ValueError: invalid literal", "TypeMismatchException", "CF_SQL_INTEGER", "CF_SQL_NUMERIC", " for CFSQLTYPE ", "cfqueryparam cfsqltype", "InvalidParamTypeException", "Invalid parameter type", "Attribute validation error for tag", "is not of type numeric", "<cfif Not IsNumeric(", "invalid input syntax for integer", "invalid input syntax for type", "invalid number", "character to number conversion error", "unable to interpret text value", "String was not recognized as a valid", "Convert.ToInt", "cannot be converted to a ", "InvalidDataException", "Arguments are of the wrong type")

采用了数字型的payload,例如原始页面id=3,随机生成了4,那么现在请求为id=7-4,利用**queryPage()**来比较页面相似度。

如果数字型返回结果为False则用字符型再试一次,例如原始页面id=3,生成id=3aaa,利用**queryPage()**来比较页面相似度。

根据结果对casting赋值(True or False)。

下面这个heavilyDynamic似乎是页面动态性过强时,直接就报错退出了

image-20211207200147236

当casting为True时,根据 URL split 拆分得到可能的后端语言,并根据不同语言给出可能的处理参数方式,有两种可能走到这个位置:

1.格式化参数出现报错信息,即上文**def _(page)**处。

2.id=3和id=3aaa通过**queryPage()**来比较页面相似度,得到的返回值为True,即页面返回结果相同(字符型)

通过这种方式可以基本判断服务端处理参数的方式。

image-20211207204739859

当result为True时,id=3和id=7-4页面相似度高(数字型),通过**getErrorParsedDBMSes()**获取可能的后端数据库类型,否则提示“not be injectable”

image-20211207210215547

在注入检测完毕后,还会进行简单的XSS和文件包含漏洞检测(很基础,基本没啥用)

image-20211207210543310

checkSqlInjection()

检测是否存在SQL注入的核心函数

**InjectionDict()**顾名思义是一个字典,用于存储一些注入成功的边界值和payload数据(边界值应该就是payload的前后缀信息)

**isDigit()isalpha()**这些是根据已知参数来对boundaries进行排序筛选

**getSortedInjectionTests()的作用是从错误消息中检测到的DBMS返回优先测试列表,测试项对应的payload在/data/xml/payloads/,在init()中通过loadPayloads()**加载

image-20211210165757597

下图两张payload的具体样例(上为联合注入,下为布尔盲注)

image-20211210172123327

image-20211210173415957

text标签包含的信息:

title:测试名称或者说测试输出标题

stype:注入类型,Sqlmap支持六种注入类型(1-6)。<1:布尔盲注;2:报错注入;3:内联注入;4:堆叠注入;5:时间盲注;6:联合注入>

level:测试等级(1-5)

risk:风险等级,可能对数据库造成的风险(1-3)

clause:表示对应的测试payload适用于哪种类型的SQL语句。<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不一定和vector中的内容相同

request:发起请求的配置。其中不可缺少payload,comment不一定有,char和columns只有UNION中存在

payload:实际测试使用的payload

comment:注释,放在后缀前

char:UNION特有字段,用于爆破字段数量

columns:UNION特有字段,用于查询字段范围

response:判断payload注入成功与否

*comparison:布尔盲注特有字段,用于对比request中请求结果

grep:报错注入特有字段,使用正则表达式匹配请求结果

time:时间盲注特有字段,等待时间

*union:处理联合注入的方法

details:根据response标签得出的结果,比如,得出数据库类型(dbms)

dbms:数据库类型

dbms_version:数据库版本

os:操作系统

第一个if判断是否停止检测。

第二个if判断是否指纹识别出后台数据库,如果没有识别将采用布尔盲注的形式对数据库进行检测。

image-20211210190454459

检测利用了**heuristicCheckDbms()**这个函数,关键代码如下

1
for dbms in getPublicTypeMembers(DBMS, True):    randStr1, randStr2 = randomStr(), randomStr()    Backend.forceDbms(dbms)    if dbms in HEURISTIC_NULL_EVAL:        result = checkBooleanExpression("(SELECT %s%s) IS NULL" % (HEURISTIC_NULL_EVAL[dbms], FROM_DUMMY_TABLE.get(dbms, "")))    elif not ((randStr1 in unescaper.escape("'%s'" % randStr1)) and list(FROM_DUMMY_TABLE.values()).count(FROM_DUMMY_TABLE.get(dbms, "")) != 1):        result = checkBooleanExpression("(SELECT '%s'%s)=%s%s%s" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), SINGLE_QUOTE_MARKER, randStr1, SINGLE_QUOTE_MARKER))    else:        result = False    if result:        if not checkBooleanExpression("(SELECT '%s'%s)=%s%s%s" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), SINGLE_QUOTE_MARKER, randStr2, SINGLE_QUOTE_MARKER)):            retVal = dbms            break

其中FROM_DUMMY_TABLE为不同数据库所特有的表

image-20211210192009838

除了这种方式,不同数据库的查询语句不同,根据payload的测试情况也能判断对应的数据库类型

判断出数据库类型以后,与用户进行简单交互以及属性配置

image-20211210194445492

接下来是联合查询,其中包括了是否指定列的范围等

。。。

判断是否指定注入技术

如果为相同的sql注入类型,则跳过

解析DBMS特有payload信息

。。。

(各种测试跳过判断)

这里根据测试来强制转换数据库类型,保证payload是对应当前数据库的

image-20211210195555393

接下来的一段代码直到布尔盲注之前,主要是在分析确定payload拼接的前后缀、参数类型以及为接下来的测试铺垫(主要是关于拼接的内容),其中用户通过**–prefix–suffix**设定的前后缀拥有更高的优先级

接下来是不同类型的注入:

布尔盲注

首先生成payload

image-20211214174351461

这里的请求包括了三种情况:

正常请求:id=1

正逻辑请求:id=1 or 1=1

负逻辑请求:id=1 or 1=2

对比包括了两种情况:

1、正常请求与负逻辑进行对比

2、负逻辑、原始界面(正常请求)和启发性测试对比

通过获取的不同页面进行比较

①正常响应与正逻辑响应是否相同

②在①的前提下比较负逻辑响应和正常响应

③发送错误的payload并与正常请求进行比较

总之就是通过对不同响应页面进行比较来判断是否存在注入点。

在这之后如果用户输入的参数中带有**–string或者–code**即指定了判断的识别字符或者状态码,也会对是否存在注入点的判断产生影响

具体代码就不贴了

报错注入

代码很短

image-20211214191454063

这里对报错注入判断逻辑比较简单,利用了正则匹配

(上文在分析Sqlmap的payload中各种标签作用时提到了报错注入特有的grep标签)

存在以下情况中的任意一种符合grep则判断为可注入:

1、页面内容错误

2、HTTP的错误响应(例如:500)

3、header中的内容

4、重定向信息

时间盲注

懂得都懂,就是设置了sleep以后响应时间是否变长来判断

image-20211214193343059

这里调用queryPage()函数时,设置了参数timeBasedCompare = True,可以跟进看下用来干嘛

当时间比较参数为真,会调用**wasLastResponseDelayed()**函数,这个函数就是在请求存在时间延迟时返回True

wasLastResponseDelayed()

结合一下给的注释来理解,这里先获取几次访问的响应时间(生成了一个响应时间的列表),并且利用**stdev()**计算出标准差

image-20211215152011217

MIN_TIME_RESPONSES = 30,如果列表中的响应时间数小于30,则if语句会一直循环直到30次

image-20211215152421341

TIME_STDEV_COEFF = 7lowerStdLimit = 7*标准差 + 响应时间列表的算术平均值

MIN_VALID_DELAYED_RESPONSE = 0.5,计算出来的下限值和最小有效延迟进行比较

image-20211215153713508

跳出上一个循环后,会询问用户是否优化延迟响应的值(time-sec = 5),如果选自了Yes则会使用**adjustTimeDelay()**函数进行计算调整

image-20211215154456661

这里又减掉了5(s),Why?

image-20211215154550048

这里采用的时间流程为:

1’ AND SLEEP(5) AND ‘xxxx’ = ‘xxxx

1’ AND SLEEP(0) AND ‘xxxx’ = ‘xxxx

1’ AND SLEEP(5) AND ‘xxxx’ = ‘xxxx

由此根据响应结果来判断是否存在时间盲注

sqlmap对时间盲注的判断是只要 超过标准的延迟时间就认为是有延迟了而不是直接判断测试的延迟时间

联合查询

这里干了几件事:

1、进行了初始配置 char = NULL,columns = 1-20

2、判断是否识别出数据库类型,如果没有可以尝试通过**–dbms**设置

3、自动扩展 UNION 查询注入技术测试的范围,因为至少发现了一种其他(潜在)技术 ps:这条不是很懂

image-20211215162625545

这里是关键

image-20211215202300874

unionTest()

image-20211215202401388

其中的核心函数**_unionTestByCharBruteforce()**

img

其中的**_findUnionCharCount()_unionConfirm()**分别来看下

_findUnionCharCount()

其中定义了**_orderByTest()函数,和手工注入利用order by测列数原理一样,这里拼接好payload以后,通过queryPage()**对比页面来判断。

image-20211215203737369

下面代码中利用了二分法,节省判断时间

如果order by失效,还会调用agent.py中的**forgeUnionQuery()**函数。这个函数的作用是输入一个查询字符串并返回其处理过的UNION ALL SELECT 查询。

1
Examples:MySQL input: CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)) FROM mysql.userMySQL output:UNION ALL SELECT NULL, CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)), NULL FROM mysql.user-- AND 7488=7488

_unionConfirm()

找到了列数后,接着寻找输出点,只需要将 UNION SELECT NULL,NULL,….,NULL 中的NULL依次替换,然后在结果中寻找插入的随机的字符串,就可以定位到输出点的位置,主要用到了**_unionPosition()**

**_unionPosition()中也调用了forgeUnionQuery()**函数

image-20211215210924072

下面是判断用户是否在检测阶段终止检测

image-20211215213508641

最后返回注入结果

其中的三个函数**checkFalsePositives()checkSuhosinPatch()checkFilteredChars()**,作用如下:

1、检查误报

2、检测Suhosin或者其他保护机制(Suhosin是一个PHP程序的保护系统)

3、检查过滤字符

image-20211215213527910

最后

附上一张sqlmap源码流程图

第一次看工具代码 可能写的有点乱 有问题欢迎指出交流