Django历史漏洞高危分析
漏洞分析列表
基础知识
参考链接: Quick Reference: Django 备忘清单
# 1. 创建虚拟环境-CVE-2019-14234
mkvirtualenv django_2.2.3
workon django_2.2.3
# 2. 下载漏洞版本
pip install Django==2.2.3
python -m django --version
# 3. 初始化环境
django-admin startproject django_2.2.3
# 文件结构
# django_2.2.3/
# manage.py # django-admin
# mysite/
# __init__.py
# settings.py # Settings/configuration for this Django
# urls.py # The URL declarations for this Django project
# asgi.py # An entry-point for ASGI-compatible web server
# wsgi.py # An entry-point for WSGI-compatible web servers
# 4. 测试是否成功
python manage.py runserver 8080
# 5.创建应用
python .\manage.py startapp polls
# 初始化数据库
# INSTALLED_APPS setting and creates any necessary database tables
python manage.py migrate
# 6. Activating models
# settings.py的INSTALLED_APPS变量中手动添加"polls.apps.PollsConfig",并进行更新
# 1. Change your models (in models.py). 2.Run python manage.py makemigrations 3.python .\manage.py migrate
python manage.py makemigrations polls
python manage.py sqlmigrate polls 0001
python .\manage.py migrate
CVE-2019-14234
漏洞概述
2019 年 8 月 1 日,django 发布了漏洞 CVE-2019-14234 公告:CVE-2019-14234 是一个 SQL 注入漏洞,影响了 django.contrib.postgres.fields.JSONField 和 django.contrib.postgres.fields.HStoreField 的关键字查询和索引查询功能。攻击者可以通过伪造带有字典扩展的 kwargs 参数来实现注入攻击。具体来说,该漏洞是由于在使用 QuerySet.filter() 函数时,未对传入的参数进行充分的验证和过滤所导致的。导致攻击者可以构造恶意数据,将其作为参数传入 filter() 函数,造成 sql 注入攻击。
Key and index lookups for django.contrib.postgres.fields.JSONField and key lookups for django.contrib.postgres.fields.HStoreField were subject to SQL injection, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to QuerySet.filter().
漏洞危害
- 危害类型: SQL注入。
- 受影响版本:
- <=2.2.4
- <=2.1.11
- <=1.11.23
漏洞分析
前置知识
JSONField和HStoreField
在公告中可以看到漏洞是由于 Django 在查询JSONField/HStoreField时未对参数进行过滤而导致的。那么什么是JSONField/HStoreField呢?在文档中可以看到,这两个都是 Django 框架中的数据库字段类型。JSONField存储的是 Json 格式的数据,而HStoreField是 Django 为 PostgreSQL 设计的字段,存储键值对数据(相当于 Python 中的字典类型)。此外,在查询时,Django 提供了以下方法进行查询(多层嵌套的 json 字段 key 通过__连接即可查询)。>>>Dog.objects.create(name='Rufus', data={ ... 'breed': 'labrador', ... 'owner': { ... 'name': 'Bob', ... 'other_pets': [{ ... 'name': 'Fishy', ... }], ... }, ... }) >>>Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None}) >>>Dog.objects.filter(data__breed='collie') <QuerySet [<Dog: Meg>]> >>>Dog.objects.filter(data__owner__name='Bob') <QuerySet [<Dog: Rufus>]>
环境搭建
- 在 QuickReference 中查看如何创建 Django 环境和项目。需要注意的是,此漏洞需要使用 Django 的默认数据库 sqlite,但漏洞信息表明该漏洞只在 postgresql 下触发。因此,需要修改默认的数据库配置并运行以下命令以初始化项目:
python manage.py startapp CVE_2019_14234。然后,在 Django 的settings.py中添加该项目,并将数据库配置修改为 postgresql。# setting.py # 添加数据库配置 # postgresql 创建对应的数据库 # CREATE DATABASE django; # CREATE USER myuser WITH ENCRYPTED PASSWORD 'mypass'; # GRANT ALL PRIVILEGES ON DATABASE django TO myuser; # ALTER ROLE myuser SET client_encoding TO 'utf8'; DATABASES = { 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'django', # 数据库名称 'USER': 'myuser', # 登录数据库用户名 'PASSWORD': 'mypass', # 登录数据库密码 'HOST': 'localhost', # 数据库服务器的主机地址 'PORT': '', # 数据库服务的端口号 } } # .... # 添加APP INSTALLED_APPS = [ "CVE_2019_14234.apps.Cve201914234Config", # CVE_2019_14234\apps.py中的类名 # ... ] - 根据公告中的漏洞信息,该漏洞与 JSONField 和 HStoreField 这两个模型字段相关,可以在历史版本的文档中找到对应的使用方法:Django 2.1: Jsonfield 和 HStoreField, 根据给出的方法在
models.py中添加对应的模型. 添加结束后进行迁移以在数据库中添加对应的对应的成员:`python manage.py makemigrations CVE_2019_14234.# CVE_2019_14234\models.py from django.contrib.postgres.fields import JSONField from django.db import models class Dog(models.Model): name = models.CharField(max_length=200) data = JSONField() def __str__(self): return self.name # CVE_2019_14234\admin.py from django.contrib import admin from CVE_2019_14234.models import Dog # # Register your models here. # 在后台注册展示创建管理类,用于后台管理页面并添加一些数据。 class DogAdmin(admin.ModelAdmin): list_display = ['name', 'data'] admin.site.register(Dog, DogAdmin) - 完成上述代码后,直接运行该项目
python3 manager.py runserver,并且在后台添加一些 json 数据用于后续测试,例如:{"breed": "husky", "owner": {"name": "David", "other_pets": [{"name": "Max"}]}} - 在 Django 中可以通过 URL 中查询参数来筛选和排序数据,因此直接在后台访问,通过单引号即可触发报错注入页面。 ![CVE_2019_14234_1.png|650]

详细分析及复现
- 跟踪调用链可以看到, 当通过 URL 查询后,Django 对应的查询语句入口如下, 通过 filter 函数进行查询,通过字典的方式将 URL 中的参数传入。
def get_queryset(self, request): # First, we collect all the declared list filters. (self.filter_specs, self.has_filters, remaining_lookup_params, filters_use_distinct) = self.get_filters(request) # Then, we let every list filter modify the queryset to its liking. # ... try: # Finally, we apply the remaining lookup parameters from the query # string (i.e. those that haven't already been processed by the # filters). qs = qs.filter(**remaining_lookup_params) - 接着进入 filter 函数内部,此时需要的注意的是我们可控的部分分为 url_key 和 url_value 两个字段,Django 对于处理这两个字段时的 sql 语句拼接过程是不一样的。
- url_key 的 sql 语句拼接是通过
KeyTransform该类生成的, 如果输入的类型是字符串,则直接使用'包裹拼接。# try_transform() class KeyTransform(Transform): operator = '->' nested_operator = '#>' # ... def as_sql(self, compiler, connection): key_transforms = [self.key_name] previous = self.lhs while isinstance(previous, KeyTransform): key_transforms.insert(0, previous.key_name) previous = previous.lhs lhs, params = compiler.compile(previous) if len(key_transforms) > 1: return "(%s %s 22),RANDOMBLOB(500000000/2),1)--.
- url_key 的 sql 语句拼接是通过
### 漏洞修复
补丁中可以看到如果是通过表别名+列名的方式传入, Django 会通过正则进行匹配,只限定字母和 `+,-,.` 符号的传入,防止通过 `"` 等方式去提前闭合语句造成注入。

## 参考链接
- [CVE-2021-35042: Potential SQL injection via unsanitized QuerySet.order_by() input](https://www.djangoproject.com/weblog/2021/jul/01/security-releases/)
- [补丁链接](https://github.com/django/django/commit/a34a5f724c5d5adb2109374ba3989ebb7b11f81f)
# CVE-2022-34265
## 漏洞概述
2022年,django 发布了漏洞 CVE-2022-34265 公告:CVE-2022-34265是一个潜在的 SQL 注入漏洞,通过将未受信任的数据用作 `Trunc(kind)` 和 `Extract(lookup_name)` 参数的值,可以导致 SQL 注入。
>[!note] CVE-2022-34265: Potential SQL injection via `Trunc(kind)` and `Extract(lookup_name)` arguments
> `Trunc()` and ` Extract () ` database functions were subject to SQL injection if untrusted data was used as a kind/lookup_name value.
>
>Applications that constrain the lookup name and kind choice to a known safe list are unaffected.
## 漏洞危害
- 危害类型:SQL 注入
- 受影响版本:
- 受影响版本< `4.0.6`
- 受影响版本< `3.2.14`
## 漏洞分析
### 前置知识
根据公告信息和[补丁中添加的测试案例](https://github.com/django/django/commit/54eb8a374d5d98594b264e8ec22337819b37443c#diff-7704d1b7fbfb3516a17d0f19338242c16eff438b3e06125015b35d62a314f638R251),直接能够看到漏洞所在函数是 `Trunc()` 和 `Extract()` ,并且也给出测试触发案例。
此外从[文档](https://docs.djangoproject.com/en/4.0/ref/models/database-functions/#trunc)中可以得知 `Trunc()` 和 `Extract()` 都是用于处理时间的函数,`Trunc` 是用于截断日期的一部分;`Extract` 用于将日期的一个组成部分提取为数字,两个函数的参数输入类似。
### 环境搭建
参考官方测试案例构造即可,如下:
```Python
# CVE_2022_34265/modesl.py
class DateModel(models.Model):
start_datetime = models.DateTimeField()
end_datetime = models.DateTimeField()
# CVE_2022_34265/views.py
from django.http import HttpResponse
from django.db.models.functions import Trunc, Extract
from CVE_2022_34265.models import DateModel
def trunc_view(request):
unit = request.GET.get('unit', 'day')
data = DateModel.objects.annotate(start_day=Trunc('start_datetime', unit))
# print(DateModel.objects.annotate(start_day=Trunc('start_datetime', unit)).query.__str__())
response = "<h1>Trunc View</h1>"
response += "<table>"
response += "<thead>"
response += "<tr><th>Start Datetime</th><th>Start day</th></tr>"
response += "</thead>"
response += "<tbody>"
for item in data:
response += "<tr><td>{}</td><td>{}</td></tr>".format(item.start_datetime, item.start_day)
response += "</tbody>"
response += "</table>"
return HttpResponse(response)
def extract_view(request):
unit = request.GET.get('unit', 'month')
data = DateModel.objects.annotate(end_month=Extract('end_datetime', unit))
response = "<h1>Extract View</h1>"
response += "<table>"
response += "<thead>"
response += "<tr><th>End Datetime</th><th>End Month</th></tr>"
response += "</thead>"
response += "<tbody>"
for item in data:
response += "<tr><td>{}</td><td>{}</td></tr>".format(item.end_datetime, item.end_month)
response += "</tbody>"
response += "</table>"
return HttpResponse(response)
详细分析及复现
从测试案例中即可看到是因为 _lookup_name_ / kind 两个参数原始是传递的是时间中 year,day 等参数用来标识具体时间的,但是 Django 没有对参数进行校验直接传入作为 sql 语句的一部分了。

值得注意的是,在使用 Django4.0.1 测试时,发现最后拼接查询的 sql 语句与网上其他漏洞分析中的语句略有不同,注入点从子句 where 到了 select 位置,如下:
SELECT "CVE_2022_34265_datemodel"."id", "CVE_2022_34265_datemodel"."start_datetime", "CVE_2022_34265_datemodel"."end_datetime", django_datetime_trunc(%vuln_data_input%, "CVE_2022_34265_datemodel"."start_datetime", 'UTC', 'UTC') AS "start_day" FROM "CVE_2022_34265_datemodel" LIMIT 21
因此可以构造如下注入语句进行注入:
# unit = year1,0,0,0),iif((select sqlite_version() like '4%'),RANDOMBLOB(500000000/2),1),date('
http://127.0.0.1:8000/CVE_2022_34265/trunc/?unit=year1%27,0,0,0),iif((select%20sqlite_version()%20like%20%22422),RANDOMBLOB(500000000/2),1),%22&func=aggregate
漏洞修复
漏洞修复和上述几个漏洞相同,对 alias 进行内容校验,过滤了一些特殊符号,防止提前对内容进行闭合,导致注入。

参考链接
CVE-2022-28347
漏洞概述
2022年,django 发布了漏洞 CVE-2022-28347 公告:CVE-2022-28347 是一个潜在的 SQL 注入漏洞,通过在 PostgreSQL 上使用 QuerySet.explain(**options) 方法时,使用适当构造的字典作为 **options 参数,可以进行 SQL 注入。
QuerySet.explain(**options) on PostgreSQL
QuerySet.explain() method was subject to SQL injection in option names, using a suitably crafted dictionary, with dictionary expansion, as the **options argument.
漏洞危害
- 危害类型:SQL 注入
- 受影响版本:
- 受影响版本<
4.0.4 - 受影响版本<
3.2.13 - 受影响版本<
2.2.28
- 受影响版本<
漏洞分析
前置知识
该漏洞只影响 postgresql 数据库,通过 Django文档可以了解到 explain 函数是用来分析 Django 对应的查询命令,并且输出对应 SQL 语句的查询计划。其等价于 postgresql 中的 EXPLAIN 命令
环境搭建
因为该漏洞需要使用的是 postgresql 数据库,之前使用的都是 sqlite 数据。因此除了新增了 views.py 外,还额外添加数据库路由配置 database_router.py,具体如下:
# CVE_2022_28347\views.py
def books_view(request):
# 获取查询执行计划
explain_dict = dict(request.GET.items())
queryset = Book.objects.all()
# 构建 HTML 内容
html_content = "<!DOCTYPE html><html> <head><title>Query Execution Plan</title></head><body><h1>Query Execution Plan</h1><pre>{query_plan}</pre></body></html>".format(query_plan=queryset.explain(format=None, **explain_dict))
return HttpResponse(html_content)
# database_router.py
class AppRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == "CVE_2022_28347":
return "postgres_db"
return None
def db_for_write(self, model, **hints):
if model._meta.app_label == "CVE_2022_28347":
return "postgres_db"
return None
# settings.py
DATABASE_ROUTERS = ['django_4_0_1.database_router.AppRouter']
详细分析及复现
从公告中可以看到漏洞输入点位于 **option 参数,该字典参数内容最终由 explain_query_prefix 函数处理。默认数据库对于有值的 **option 参数是不会处理的,并且会抛出异常。
Django 中 postgresql 类中重载了该函数。Django 在从 **options 中提取 key 值的时候,直接将 key 提取进行了拼接作为了后面 explain 查询的中的参数。

sql 语句如下
EXPLAIN (%vuln_data_input%) SELECT "CVE_2022_28347_book"."id", "CVE_2022_28347_book"."title", "CVE_2022_28347_book"."author" FROM "CVE_2022_28347_book"
EXPALIN 默认是不执行对应的语句的。但是在 postgresql 的文档中可以看到,EXPALIN 中有一个 ANALYZE 参数,添加该参数后,postgresql 会执行命令并显示实际运行时间和其他统计信息,因此只能通过该参数闭合然后进行时间盲注,构造如下语句:
# http://127.0.0.1:8000/CVE_2022_28347/?ANALYZE)%20SELECT%20pg_sleep(5)--=1
EXPLAIN (ANALYZE) SELECT pg_sleep(5)--
漏洞修复
- Django 在补丁中对
**option传入的参数进行了校验,限定了 EXPLAIN 函数支持的那个几个参数
- 在 Django 的
explain (format=None, **options)中会直接对传入的options进行正则校验, 限定了传入的字符类型