安基网 首页 资讯 安全报 查看内容

「漏洞预警」Django SQL注入漏洞(CVE-2019-14234)

2019-11-1 09:54| 投稿: xiaotiger |来自: 互联网


免责声明:本站系公益性非盈利IT技术普及网,本文由投稿者转载自互联网的公开文章,文末均已注明出处,其内容和图片版权归原网站或作者所有,文中所述不代表本站观点,若有无意侵权或转载不当之处请从网站右下角联系我们处理,谢谢合作!

摘要: 漏洞描述当使用用户可控的数据作为参数,以 **kwargs 的形式传入 QuerySet.filter() 函数,对 django.contrib.postgres.fields.JSONField 进行键/索引查找,或对 django.contrib.postgres.fields.HStoreField 进行键查找时,将会导致 SQL 注入。影响范围Django 主开发分支Django 2.2.x < 2.2.4Django ...

漏洞描述

当使用用户可控的数据作为参数,以 **kwargs 的形式传入 QuerySet.filter() 函数,对 django.contrib.postgres.fields.JSONField 进行键/索引查找,或对 django.contrib.postgres.fields.HStoreField 进行键查找时,将会导致 SQL 注入。

影响范围

  • Django 主开发分支
  • Django 2.2.x < 2.2.4
  • Django 2.1.x < 2.1.11
  • Django 1.11.x < 1.11.23


作为铁杆Django用户,发现昨天Django进行了更新,且修复了一个SQL注入漏洞。在我印象里这应该是Django第一个SQL注入漏洞,且的确是可能在业务里出现的漏洞,于是进行了分析。

0x01 什么是JSONField

Django是一个大而全的Web框架,其支持很多数据库引擎,包括Postgresql、Mysql、Oracle、Sqlite3等,但与Django天生为一对儿的数据库莫过于Postgresql了,Django官方也建议配合Postgresql一起使用。

相比于Mysql,Postgresql支持的数据类型更加丰富,其对JSON格式数据的支持也让这个关系型数据库拥有了NoSQL的一些特点。在Django中也支持了Postgresql的数据类型:

  • JSONField
  • ArrayField
  • HStoreField

这三种数据类型因为都是非标量,且都能用JSON来表示,我下文就用JSONField统称了。

我们可以很简单地在Django的model中定义JSONField:

from django.db import models
from django.contrib.postgres.fields import JSONField
class Collection(models.Model):
name = models.CharField(max_length=128, default='default name')
detail = JSONField()
def __str__(self):
return self.name

然后,我们在视图中,就可以对detail字段里的信息进行查询了。

比如,detail中存储了一些文章信息:

{
"title": "Article Title",
"author": "phith0n",
"tags": ["python", "django"],
"content": "..."
}

我要查询作者是phit0n的所有文章,就可以使用Django的queryset:

Collection.objects.filter(detail__author='phith0n').all()

非常简单,和我们正常的queryset完全一样,只不过这里的detail是一个JSONField,而下划线后的内容代表着JSON中的键名,而不再是常规queryset时表示的“外键”。

同理,如果我想查询所有含有python这个tag的文章,可以这样编写queryset:

Collection.objects.filter(detail__tags__contains='django').all()

JSONField的强大让我们能灵活地在关系型数据库与非关系型数据库间轻松地切换,因此在我们的很多业务中都会使用到这个功能。

0x02 SQL注入漏洞何来

那么,是什么问题导致了这个漏洞?

我们直接看到JSONField的实现:

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)

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

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

如果你不知道,可以参考一下这篇文档:https://docs.djangoproject.com/en/2.2/ref/models/lookups/

  1. Lookup
  2. Transform

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

.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语句表示就是:

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对象:

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

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

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

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

0x03 如何复现这个漏洞

分析了原因,复现的方法就呼之欲出了。

根据上面的分析可知,transform是生成SQL查询中“键名”的部分,那么如果我们控制了queryset查询的键名,即可注入任意SQL语句了。

但是熟悉Django的同学也应该知道,Django的queryset使用方法是编写如下查询语句:

.filter(detail__author='phith0n')

这个detail__author用户是无法控制的,通常只有值才能被控制。

但是如果你参与过pwnhub在2017年的一场比赛,应该记得我当时构造了一种比较特殊的查询方法,ORM注入:


就是如果你能控制filter方法的参数名,就能通过外键的方式来获取其他表的一些敏感信息。

当时的场景就是,开发者把用户传入的整个对象都传入filter函数了:

data = json.loads(request.body.decode())
stu = models.Student.objects.filter(**data).first()

此时,用户即可控制filter的键名,在这种情况下,借助我们这次的漏洞即可完成SQL注入利用。

有的人可能觉得这种场景不是很常见,我们来思考一个更加常见的场景。

环境搭建

  • Python 2.7.10
  • Postgresql(docker)
  • 漏洞demo
  • 漏洞代码片段:

为了触发漏洞,将http get里面的参数字典以函数参数的形式传过去,**的作用就是可以将字典转为函数参数,例如filter(**{'a':'1','b':'2'})与filter(a=1,b=2)是等价的

漏洞复现

MD5 PoC:

http://127.0.0.1:8000/select?info__test%27+%3d+%27%22a%22%27)%20and%207778%3dCAST((SELECT%20md5(version()))::text%20AS%20NUMERIC)--

Version EXP:

http://127.0.0.1:8000/select?info__test%27+%3d+%27%22a%22%27)%20and%207778%3dCAST((SELECT%20version())::text%20AS%20NUMERIC)--

原理分析

先看看官方补丁,定位到关键代码处:

定位到该函数 as_sql(jsonb.py:L97):

通过函数名推测该函数是用来生产相关SQL语句的,后来查了一下,这个函数主要是为filter()函数提供json查询支持。

举个例子:现在数据库里存了一些这样的数据

如果我要查info字段里面json数据里name键为rivaill2的数据就可以用如下代码去查

XXXmodel.ob jects.filter(info__name='rivaill1')

最终上面的as_sql会生成如下一段SQL:

这是postgresql进行json查询的语法

再回来看看存在漏洞的代码:

大概的代码逻辑就是会将key强转一下,如果强转不成功就在两侧加个单引号,但是这里面没有经过任何过滤,追踪整个调用链也没有发现针对这个key的过滤,然后将字段名、操作符、key拼接了一下就return出去了。

所以这里的注入点实际上是在json的key里面,想要这个输入点可控就需要filter函数中的参数key可控,这也是为什么demo中要将http get以**kwargs 的形式传入filter函数的原因。

测试一下:

可以看到确实可控了,info是指定的字段,name是info字段的json数据中的key,最终解析到filter函数里面就是filter(info__name='rivaill2')

往key后面加个单引号就能触发注入

修复措施

看一下官方的补丁:

其实关键就在最后一行,lookup(也就是key)没有直接与前面的字段名和操作符拼接了,而是放到了第二个返回值里面和params拼接,最后追踪了一下调用链发现第二个返回值里面的数据都会作为参数化查询的value被代入进去,这样就避免了注入。

参考:

https://www.leavesongs.com/PENETRATION/django-jsonfield-cve-2019-14234.html

https://nosec.org/home/detail/2831.html

https://www.freebuf.com/vuls/210257.html



小编推荐:欲学习电脑技术、系统维护、网络管理、编程开发和安全攻防等高端IT技术,请 点击这里 注册账号,公开课频道价值万元IT培训教程免费学,让您少走弯路、事半功倍,好工作升职加薪!

本文出自:https://www.toutiao.com/a6721592470653633038/

免责声明:本站系公益性非盈利IT技术普及网,本文由投稿者转载自互联网的公开文章,文末均已注明出处,其内容和图片版权归原网站或作者所有,文中所述不代表本站观点,若有无意侵权或转载不当之处请从网站右下角联系我们处理,谢谢合作!


鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论

 最新
返回顶部