背景
时隔了好好好几个月。。。继上次文章 初探Sqlmap(一)中立下的Flag:下一篇出sqlmap原理相关的介绍😂😂,由于各种原因。。。今天才终于完成了┑( ̄。。 ̄)┍ 。。不多说了。。。
什么是SQL注入?
指在程序的输入数据中添加额外的SQL语句。就好比有一个登录输入框,在输入帐号的的输入框中,输入了 'arvin' or 1=1
,对应到数据库层面,sql语句可能就如下实例:
1 | # 正常的sql语句 |
为什么会存在SQL注入?
由于程序对 用户输入数据的合法性没有判断或过滤不严,攻击者可以在程序中事先定义好的 查询语句的结尾上添加额外的SQL语句。
1 | # python中一种存在SQL注入的sql写法, 由于没有对传入的参数 name 进行任何的校验,并且是简单的动态拼接查询语句,导致可以进行SQL注入。 |
存在SQL注入的影响
可以在管理员不知情的情况下,实现欺骗 数据库服务器 执行非授权的任意查询,从而进一步得到相应的数据信息。
1 | # http://127.0.0.1:5000/bad_sql?id=1 |
Sql注入的主要流程
SQL注入点探测。
探测SQL注入点是关键的一步,通过适当的分析应用程序,可以判断什么地方存在SQL注入点。通常只要 带有输入提交的动态网页,并且 动态网页访问数据库,就可能存在SQL注入漏洞。如果程序员信息安全意识不强,采用动态构造SQL语句访问数据库,并且对用户的输入未进行有效性验证,则存在SQL注入漏洞的可能性很大。一般通过页面的报错信息来确定是否存在SQL注入漏洞。
- 疑问1: 什么样的报错信息表示可能存在SQL注入?
- 疑问2: 怎么样去判断是否存在SQL注入点?
收集后台数据库信息。
不同数据库的注入方法、函数都不尽相同,因此在注入之前,我们先要判断一下数据库的类型。判断数据库类型的方法很多,可以输入特殊字符,如单引号,让程序返回错误信息,我们根据错误信息提示进行判断;还可以使用特定函数来判断,比如输入“1 and version()>0”,程序返回正常,说明
version()
函数被数据库识别并执行,而version()
函数是MySQL
特有的函数,因此可以推断后台数据库为MySQL。猜解用户名和密码。
数据库中的表和字段命名一般都是有规律的。通过构造特殊SQL语句在数据库中依次猜解出表名、字段名、字段数、用户名和密码。(疑问3: 如何构造的特殊SQL语句,猜解出来表名、字段名以及密码等)
查找Web后台管理入口。
入侵和破坏。
一般后台管理具有较高权限和较多的功能,使用前面已破译的用户名、密码成功登录后台管理平台后,就可以任意进行破坏,比如上传木马、篡改网页、修改和窃取信息等,还可以进一步提权,入侵Web服务器和数据库服务器。
环境准备
在介绍工具之前,可以本地搭建一个简单的web服务,需要依赖 Flask
、mysql
、python
等,请自行百度安装。
Sqlmap工具的介绍
Sqlmap 实例演练
用命令行实际例子来演示一遍, 通过命令行来注入,并且获取对应的用户名和密码。(下面实例中的参数可以参考:参考链接)
1 | # 查找注入点 |
PS: 更详细的一些SQL注入基础原理可参考:SQL注入基础,里面举了较多的注入实例。关于Sqlmap 的命令行模式与 API模式的区别以及优缺点,在 初探Sqlmap(一) 中已经描述过,此处不再重复。
SqlmapAPI 介绍
SqlmapAPI 的启动方式:
python sqlmapapi.py -s -H "0.0.0.0" -p 8775
SqlmapAPI 的脚本主要核心流程如下图:
Sqlite3 数据库:SQLite是一种嵌入式数据库,它的数据库是一个文件。
WSGI application:Web服务器网关接口(Python Web Server Gateway Interface)
使用API模式进行sql注入测试时候,主要使用如下几个API接口(具体如何调用以及为什么要使用该方式可参考:细说Sqlmap)
1
2
3
4
5# 创建一个新的扫描任务
# 指定任务进行扫描
# 查看指定任务的状态
# 查看指定任务的执行结果
# 查看指定任务的扫描执行日志
SqlmapAPI Task执行原理
此处仅介绍 /scan/<taskid>/start
,主要通过 DataStore.tasks[taskid].engine_start()
执行指定任务执行扫描。
1 | def engine_start(self): |
从上面的代码来看,最终默认还是使用的 sqlmap.py
脚本来进行的任务扫描。(可以通过打印出源代码中 configFile
的路径,来查看每次任务的配置参数是什么。)
Sqlmap核心流程梳理
(一)先是进行一系列的系统环境准备(sqlmap运行的系统版本)
1 | dirtyPatches() |
(二)对用户传入的参数进行解析以及构建对应的测试Sql注入语句
1 | args = cmdLineParser() |
具体执行的Sql注入测试语句样式如下:
(三)进行注入测试,找出注入点
测试的Sql语句类型是哪些?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23"""
Valid values:
1: Boolean-based blind SQL injection 基于布尔的盲注,即可以根据返回页面判断条件真假的注入;
2: Error-based queries SQL injection 基于报错注入,即页面会返回错误信息,或者把注入的语句的结果直接返回在页面中;
3: Inline queries SQL injection 基于内联视图注入, 内联视图能够创建临时表,在处理某些查询情况时十分有用。
4: Stacked queries SQL injection 堆查询注入,可以同时执行多条语句的执行时的注入。
5: Time-based blind SQL injection 基于时间的盲注,即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断;
6: UNION query SQL injection 联合查询注入,可以使用union的情况下的注入;
"""
# 根据具体的用户传入的 level 和risk 来过滤哪些sql注入测试语句需要执行。
# 具体执行代码在 check.py 中进行执行 checkSqlInjection() 方法,部分代码如下:
...
if test.risk > conf.risk:
debugMsg = "skipping test '%s' because the risk (%d) " % (title, test.risk)
debugMsg += "is higher than the provided (%d)" % conf.risk
logger.debug(debugMsg)
continue
# Skip test if the level is higher than the provided (or default) value
if test.level > conf.level:
debugMsg = "skipping test '%s' because the level (%d) " % (title, test.level)
debugMsg += "is higher than the provided (%d)" % conf.level
logger.debug(debugMsg)答疑1: 注入过程中,什么样的报错信息表示可能存在SQL注入?
1
2
3
4
5# 通过 heuristicCheckSqlInjection() 方法去判读是否存在Sql注入。
check = heuristicCheckSqlInjection(place, parameter)
# 如果结果返回如下内容则表示可能存在SQL注入, 因为参数已经走到了数据库层面。
pymysql.err.ProgrammingError: (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'")()..\',.\' at line 1')答疑2: 注入过程中,怎么样去判断是否存在SQL注入点?
1
2
3# Store here the details about boundaries and payload used to successfully inject
# 确认哪些test语句可以进行注入
injection = checkSqlInjection(place, parameter, value)
(四)sqlmap 找到注入点后的后置操作
1 | # sqlmap 找到注入后, 会进行如下操作将该URL的结果存储下来,避免反复的去扫描 |
1 | [16:12:19] [INFO] testing connection to the target URL |
(五)sqlmap 找到注入点后, 如何去获取更多的数据呢?
答疑3: 如何构造的特殊SQL语句,猜解出来库名、表名、字段名以及密码等?
获取所有的库名,具体方法的调用栈如下:(有兴趣的可自行进行 DEBUG 分析)
通过构建出
select schema_name from information_schema.schemata
语句,来获取所有的库信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# plugins/generic/databases.py getDbs() 其中部分核心代码如下:
conf.dumper.dbs(conf.dbmsHandler.getDbs())
# 构建的获取库名信息的sql语句
query = 'SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA'
# 获取到了具体的db信息
values = inject.getValue(query, blind=False, time=False)
# values 返回的结果大致如下
res:{
...
"version_end_time": null,
"version_start_time": "qqkbqinformation_schemaqvjpq"
},
value = parseUnionPage(output) # 将上面的结果解析出正确的db名称
# res: ['mysql', 'information_schema', 'performance_schema', 'sys']
# 如果已经获取出来了的情况下, 会直接存储在本地的sqlit3的表中。避免后续继续扫描。
def hashDBWrite(key, value, serialize=False)
# 如果未获取过, action中进行实际的注入来获取对应的数据, 比如:_oneShotUnionUse 中最终拼凑出的 query,直接可以查出对应的库信息。拼凑出来的sql注入语句如下:
'id=111 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,CONCAT(0x7176627671,IFNULL(CAST(schema_name AS CHAR),0x20),0x716b717a71),NULL,NULL,NULL,NULL FROM INFORMATION_SCHEMA.SCHEMATA-- -__PAYLOAD_DELIMITER__'获取表名以及表内具体的数据
1
2# 原理同库名的获取, 但是在db步骤后,这些数据都被写入到了本地的sqlit3中了, 所以基本都是直接通过如下方法获取到结果了。
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True)
(六)破解mysql的登录账号密码
1 | # 如果在密码很简单的情况下, 会通过撞hash猜出来,比如:为了演示,root的密码是123456, 则直接破解出来了, 但是其他的用户密码较复杂, 尝试了下稍微复杂点的都无法破解,如:abc123456。 |
wordlist.tx_ 文件中的内容部分截取如下图:
如果存在破解成明文的密码, 则如下图中的 clear-text password
打印出来,否则打印出来的是加密的值。
团队内部实践
- 通过界面快速新增任务;
- 查询已有SQL注入任务(可界面执行单个任务以及查看该任务最近一次的结果);
- 通过拉取YAPI相关接口信息,来跟进已经接入的接口进度;
- 通过Jenkins定时任务触发,构建定制化的参数任务(定制化参数任务中,提高了扫描的risk以及level值)。
总结
通过此次总结,也算是把整个Sqlmap工具与公司业务结合在一起, 虽然通过扫描后还未发现任何问题(如果真的扫描出问题,那就是比较好的实践结果了~( ̄▽ ̄~)(~ ̄▽ ̄)~ ),整个过程也算是真正的把技术与工作的业务相结合, 过程中也是收获较多。