Django SQL注入历史漏洞分析
2022-6-8 14:54:48 Author: xz.aliyun.com(查看原文) 阅读量:28 收藏

考试前翻Python的组件漏洞时看到过Django存在SQL注入漏洞, 考完后抽空分析几个相关的漏洞, 分别是CVE-2020-7471CVE-2021-35042CVE-2022-28346.

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development, so you can focus on writing your app without needing to reinvent the wheel. It’s free and open source.

漏洞简介

Django 1.11 before 1.11.28, 2.2 before 2.2.10, and 3.0 before 3.0.3 allows SQL Injection if untrusted data is used as a StringAgg delimiter (e.g., in Django applications that offer downloads of data as a series of rows with a user-specified column delimiter). By passing a suitably crafted delimiter to a contrib.postgres.aggregates.StringAgg instance, it was possible to break escaping and inject malicious SQL.

漏洞环境

漏洞分析

在漏洞描述中说明该漏洞的核心是StringAgg聚合函数的delimiter参数存在SQL注入漏洞. 通过查找Djangocommit记录, 在官方对的修复代码中可以看到, 漏洞函数位于from django.contrib.postgres.aggregates import StringAgg模块之中.

官方修复通过引入from django.db.models import Value中的Value来处理来防御该注入漏洞:

delimiter_expr = Value(str(delimiter))

跟进django.db.models中的Value函数, 在注释中可以看到, Value函数会将处理过后的参数加入到sql parameter list, 之后会进过Django内置过滤机制的过滤, 从而来防范sql注入漏洞.

由于漏洞点是位于StringAgg聚合函数的delimiter参数, 在官方文档中对该聚合函数进行了说明, 简单来说它会将输入的值使用delimiter分隔符级联起来.

通过Fuzz发现delimiter为单引号时会引发报错, 且通过打印出的报错信息可以看到, 单引号未经过任何转义就嵌入到了sql语句中.

def fuzz():
    symbol_str = "[email protected]#$%^&*()_+=-|\\\"':;?/>.<,{}[]"
    for c in symbol_str:
        results = Info.objects.all().values('gender').annotate(mydefinedname=StringAgg('name',delimiter=c))
        try:
            for e in results:
                pass
        except IndexError:
            pass
        except Exception as err:
            print("[+] 报错信息: ", err)
            print("[+] 漏洞分隔符: ", c)

根据报错信息, 在_execute函数中打断点.

当遍历完数据库中的数据后, 进行Fuzz操作, 观察加入了delimiter为单引号取值的sql.

由于此时sql是个字符串, 因此会产生转义号, 该sql语句在postgres中的执行语句为:

SELECT "vuln_app_info"."gender", STRING_AGG("vuln_app_info"."name", ''') AS "mydefinedname" FROM "vuln_app_info" GROUP BY "vuln_app_info"."gender"

接着尝试将delimiter设置为')--, 使得其注释掉后面的语句, 查看报错信息, 可以看到成功注释了FROM语句.

构造exp如下:

-\') AS "demoname" FROM "vuln_app_info" GROUP BY "vuln_app_info"."gender" LIMIT 1 OFFSET 1 --

漏洞简介

Django 3.1.x before 3.1.13 and 3.2.x before 3.2.5 allows QuerySet.order_by SQL injection if order_by is untrusted input from a client of a web application.

漏洞环境

漏洞分析

根据漏洞信息, 先跟进order_by函数, 该函数先调用了clear_ordering函数清除了Query类中的self.order_by参数, 接着调用add_ordering函数增加self.order_by参数.

通过order_by函数传入的参数为数组, 漏洞环境中接收参数的代码对应的SQL语句如下:

query = request.GET.get('order_by', default='vuln')
res = User.objects.order_by(query)
SELECT "app_user"."vuln", "app_user"."order_by" FROM "app_user" ORDER BY "app_user"."order_by" ASC, "app_user"."vuln" ASC

跟进add_ordering函数, 函数对ordering做递归之后进行了判断, 如果item为字符串, 则继续进行如下五次判断:

  • if '.' in item: 判断是否为带列的查询, SQL语句中允许使用制定表名的列, 例如order by (user.name), 即根据user表下的name字段进行排序. 该方法将在Django 4.0之后被删除, 因此判断成功之后通过warning警告, 之后进行continue.
  • if item == '?': 当item的值为字符串?时, 则会设置order by的值为RAND(), 表示随机显示SQL语法的返回数据, 之后进行continue.
  • if item.startswith('-'): 当item开头为字符串-时, 则将order by查询的结果接上DESC, 表示降序排列, 默认的字符串则会接上ASC正序排列, 同时去除开头的-符号.
  • if item in self.annotations: 判断时候含有注释标识符, 有的话直接continue.
  • if self.extra and item in self.extra: 判断是否有额外添加,有的话直接continue.

经过五次判断之后, 进入到self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)函数判断当前的item是否为有效的列名, 之后将所有的ordering传入到Query类中的self.order_by参数中进行后续处理.

在第一次判断中, if '.' in item进行判断能够确保order by查询能够更好地兼容何种形式的带列的查询, 但是判断是否为带表的查询之后, 如果判定为带表查询则进行continue, 而continue则直接跳过了self.names_to_path的对列的有效性检查. 跟进处理带字符串.的代码, 位于文件django/db/models/sql/compiler.pyget_order_by函数, 核心代码如下:

if '.' in field:
    table, col = col.split('.', 1)
    order_by.append((
            OrderBy(
                RawSQL('%s.%s' % (self.quote_name_unless_alias(table), col), []),
                descending=descending
            ), False))
    continue

上述代码中, 函数self.quote_name_unless_alias处理表名, 同样使用字典来强制过滤有效的表名, 而后面的列面则恰好未经过过滤, 则可以构造闭合语句进行SQL注入.

参数app_user.name) --最终传入数据库的语句为:

SELECT `app_user`.`id`, `app_user`.`name` FROM `app_user` ORDER BY (`app_user`.name) --) ASC LIMIT 21

使用右括号)进行闭合之后进行堆叠注入, 基本的payload如下: http://127.0.0.1:8000/vuln/?order_by=vuln_app_user.name);select%20updatexml(1,%20concat(0x7e,(select%[email protected]@version)),1)%23

漏洞简介

An issue was discovered in Django 2.2 before 2.2.28, 3.2 before 3.2.13, and 4.0 before 4.0.4. QuerySet.annotate(), aggregate(), and extra() methods are subject to SQL injection in column aliases via a crafted dictionary (with dictionary expansion) as the passed **kwargs.

漏洞环境

漏洞分析

查找Djangocommit记录, 在官方对的修复代码中可以看到测试用例.

由漏洞描述不, 跟进漏洞点annotate函数, 在annotate函数中首先会调用_annotate并传入kwargs.

annotate函数在完成对kwargs.values()合法性校验等一系列操作后, 将kwargs更新到annotations中, 随后遍历annotations中的元素调用add_annotation进行数据聚合.

跟进add_annotation函数, 继续调用resolve_expression解析表达式, 在此处并没有对传入的聚合参数进行相应的检查.

继续跟进, 最终进入到db.models.sql.query.py:resolve_ref, resolve_ref会获取annotations中的元素, 并将其转换后带入到查询的条件中, 最后其结果通过transform_function聚合到一个Col对象中.

接着, 返回db.models.query.py:_annotate, 执行sql语句, 将结果返回到QuerySet中进行展示.


文章来源: https://xz.aliyun.com/t/11422
如有侵权请联系:admin#unsafe.sh