Django sql注入 CVE-2022-34265

发布于 2022-07-17  387 次阅读


0x00 前言

好久没发博客了,其实本地也写了一些,只是没写完整就没有发上来,也不知道还会不会补上(懒

最近正好Django爆了一个sql注入的漏洞,也没有分析过python的框架漏洞,就简单来看一看。

0x01 环境搭建

Django Extract & Trunc SQL注入影响版本:3.2.0-3.2.144.0.0-4.0.6,选择一个泛微内的版本即可。

代码的话我是直接拿p牛的vulhub中的代码来用的。https://github.com/vulhub/vulhub/tree/master/django/CVE-2022-34265。

视图代码在vuln/views.py中,这里只给了Trunc

def vul(request):
    create_log(request)
    date = request.GET.get('date', 'minute')
    objects = list(WebLog.objects.annotate(time=Trunc('created_time', date)).values('time').order_by('-time').annotate(count=Count('id')))
    return JsonResponse(data=objects, safe=False)

我们都加上,然后在vuln/urls.py中注册一下就行了。

def vul_trunc(request):
    create_log(request)
    date = request.GET.get('date', 'minute')
    objects = list(
        LogTable.objects.annotate(time=Trunc('created_time', date)).values('time').order_by('-time').annotate(
            count=Count('id')))
    return JsonResponse(data=objects, safe=False)


def vul_extract(request):
    create_log(request)
    date = request.GET.get('date', 'minute')
    objects = list(
        LogTable.objects.annotate(time=Extract('created_time', date)).values('time').order_by('-time').annotate(
            count=Count('id')))
    return JsonResponse(data=objects, safe=False)

浅试一下,传一个双引号过去,Extract输出报错信息,You have an error in your SQL syntax,具体的后面再说。

image-20220717202701365

再来看看Trunc,同样传一个双引号过去,却是输出了正确的响应内容没有报错。

image-20220717202853799

是因为我在这里后端数据库选择的是MySQLTrunc函数的注入在PostgreSQLSqlite下存在,其他数据库不存在,Extract函数的注入在所有数据库下都存在。

0x02 分析

分析这两个函数的注入,首先得知道这两个东西是拿来干嘛的。

Extract用来处理时间,比如我们通过Extract('created_time', 'year')来对时间进行处理,created_time对应的就是数据库表中的created_time字段,我们对值2022-07-17T20:47:00处理之后就能得到第二个参数year对应的年份,即2022

Trunc同样是对时间进行处理,用法也差不多,Trunc('created_time', 'year'),但是这样就会得到2022-01-01T00:00:00,即处理得到时间的年份,如果我们传入的第二个参数为day,那么得到的结果就是2022-07-17T00:00:00

接下来就来分析一下Extract的注入

首先在Extract类的__init__()方法处打下断点。web端传入一个简单的参数year",这里的lookup_name就是我们传入的参数。

    def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
        if self.lookup_name is None:
            self.lookup_name = lookup_name
        if self.lookup_name is None:
            raise ValueError('lookup_name must be provided')
        self.tzinfo = tzinfo
        super().__init__(expression, **extra)

在 Django 框架自带的 ORM 模型中,当进行 SQL 查询操作时,将调用 django.db.models.query.pyQuerySet 类中对应方法进行处理。

接下来运行到django.db.models.sql.compiler.py#SQLCompilerexecute_sql()方法,对数据库进行查询并返回结果。

进入as_sql()

image-20220717223031643

由于查询的字段`vuln_logtable`.`created_time`DateTimeField类型,所以isinstance(lhs_output_field, DateTimeField)true,进入connection.ops.datetime_extract_sql()

image-20220717223800757

随后就是根据我们传入的参数,即上面lookup_name保存的值来返回不同的sql语句。当我们输入的内容不为他限定的那几个值时就会进入else"EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name),我们的lookup_type仅仅变为大写后就拼入sql语句了。得到EXTRACT(YEAR" FROM `vuln_logtable`.`created_time`),我们的双引号就进去了,没有转义,也没有东西和他闭合,放到sql语句中执行就会直接报错。

image-20220717223947025

最后执行的sql语句如下:

SELECT EXTRACT(YEAR" FROM `vuln_logtable`.`created_time`) AS `time`, COUNT(`vuln_logtable`.`id`) AS `count` FROM `vuln_logtable` GROUP BY EXTRACT(YEAR" FROM `vuln_logtable`.`created_time`) ORDER BY `time` DESC

这里的YEAR"就是我们的输入,"没有转义也没有东西和他闭合,自然就注入成功然后报错了。

那么我们就可以简单粗暴一点,直接YEAR FROM `vuln_logtable`.`created_time`) AS `time`, COUNT(`vuln_logtable`.`id`) AS `count` FROM `vuln_logtable` GROUP BY EXTRACT(YEAR FROM `vuln_logtable`.`created_time`) union select 1,version()来进行注入。

但是太长了缺乏美感,而且如果url长度或者我们输入字段的长度存在限制的话,这样就太容易超长了。而且如果报错信息没有开,我们就会不知道对应的表名。

那么就来构造一下sql语句来精简一下长度。首先我们需要满足EXTRACT()函数能够执行。它的语法为:EXTRACT(unit FROM date)date 参数是合法的日期表达式。unit 参数可以是SECONDMINUTEHOURDAY等值,那么在MySQL中得到一个日期可以使用NOW()函数最为精简。

如此一来就可以省略掉大段的sql语句,直接利用
```DAY from NOW()),1 union select 1,version()%23```就可以进行注入了。

0x03 补丁

看一下官方的修复版本,https://github.com/django/django/commit/54eb8a374d5d98594b264e8ec22337819b37443c。

首先定义了一个正则表达式extract_trunc_lookup_pattern,然后在as_sql()中,会对输入进行一个正则检查,只能存在数字与字母,就可以将恶意数据给过滤掉了。

image-20220717235608602