
Базовый анализ CVE-2021-35942. SQL-инъекция в Django.
Django — это веб-фреймворк с открытым исходным кодом, написанный на Python и построенный по модели MVC (Model - View - Controller). Изначально он был создан для управления новостными веб-сайтами, принадлежащими издательской корпорации Lawrence, как программное обеспечение CMS (Content Management System).
В Django версии 3.1.x -> 3.1.13 и версии 3.2.x -> 3.2.5 существует уязвимость SQL-инъекции.
Причина этой уязвимости заключается в том, что функция фильтрации входных данных, контролируемых пользователем, в QuerySet.order_by() недостаточна для защиты от атак SQL-инъекций. Эта уязвимость может быть использована злоумышленниками для выполнения несанкционированных действий, приводящих к утечке конфиденциальных данных.
| CVE - ID | CVE-2021-35042 |
|---|
| Severity | 9.8 - CRITICAL |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| Vulnerability Publication Date | 1/7/2021 |
| Affected Software | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Require Authentication | No required |
В Django создание таблиц и определение полей в базе данных выполняется путем объявления класса модели в файле models.py. В этом примере мы объявляем таблицу с именем Wolf и поле с именем name.


ORM-фреймворк, встроенный в Django, используется для работы с базой данных, и результатом запроса является набор, который называется QuerySet.
order_by(fields)
По умолчанию order_by() возвращает QuerySet, отсортированный в порядке, указанном в опции ordering в Meta модели. Мы можем переопределить условие order_by в каждом запросе, используя метод order_by().
Пример
wolves = Wolf.objects.order_by('-name', 'id')
Результат приведенного выше запроса будет отсортирован по убыванию по полю name, затем по возрастанию по id. Знак минуса перед именем поля name указывает на сортировку результатов по убыванию.
Следующий пример сортирует возвращаемые результаты по полю, полученному от пользователя; если значение не передано, сортировка выполняется по полю
id.
Результат

В версиях 3.1 и 3.2 Django позволяет комбинировать метод запроса с именем таблицы в запросе order_by. Это также является основной причиной данной уязвимости.
Передача имени таблицы дает нам тот же результат, что и передача обычного имени поля
cve202135042_wolf — это имя таблицы
Сначала приложение напрямую вызывает функцию order_by(); код, обрабатывающий функцию order_by(), определен в:
django/db/models/query.py

Функция order_by() выполняет две задачи
- Удаляет все текущие методы, вызываемые order_by(), и удаляет параметр по умолчанию, переданный, когда order_by получает другое значение.
- Передает параметр в order_by. Функция
add_ordering()выполняет это
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
Параметр, передаваемый в add_ordering(), представляет собой массив.
Например, когда параметр передается следующим образом:
wolves = Wolf.objects.order_by( 'name' , 'id' )В этом случае приложение преобразует его в следующий запрос к базе данных:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
При передаче функция add_ordering проверяет каждый элемент массива; если это string, проверяются следующие 5 случаев:
if '.' in item:Проверяется, является ли это запросом с именем столбца, и указано ли имя таблицы для этого столбца в SQL-операторе. Если да, выдается предупреждение и выполняетсяcontinue.if item == '?':Если значение элемента — знак '?', результаты выводятся в случайном порядке, выполняетсяcontinue.if item.startswith('-'):Если элемент начинается с символа '-', результаты запроса сортируются по убыванию (DESC).if item in self.annotations:Проверяется, содержит ли он комментарий; если да, выполняетсяcontinue.if self.extra and item in self.extra:Определяется, есть ли дополнительные параметры; если есть, выполняетсяcontinue.
После 5 проверок параметр передается в функцию self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) для дальнейшей проверки, является ли он допустимым именем столбца; затем, если он допустим, он добавляется в self.ordering класса Query для дальнейшей обработки.
ORM Django очень строго фильтрует данные, вставляемые в запрос, но изменение исходного кода, приведшее к SQL-инъекции, связано с тем, что автор предположил, что если имя столбца является UUID (Universal Unique Identifier), то запрос order_by не может быть выполнен.
То есть, если переданные данные имеют формат xxx-xxx-xxx-xxx (формат UUID), запрос не может быть выполнен.
Код до внесения изменений
# django/db/models/sql/constants.py
ORDER_PATTERN = _lazy_re_compile ( r '\?|[-+]?[.\w]+$' )
# django/db/models/sql/query.py
def add_ordering ( self , * ordering ):
errors = []
for item in ordering :
if isinstance ( item , str ) and ORDER_PATTERN . match ( item ):
if '.' in item :
warnings . warn (
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item ,
category = RemovedInDjango40Warning ,
stacklevel = 3 ,
)
elif not hasattr ( item , 'resolve_expression' ):
errors . append ( item )
if getattr ( item , 'contains_aggregate' , False ):
raise FieldError (
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s ' % item
)
if errors :
raise FieldError ( 'Invalid order_by arguments: %s ' % errors )
if ordering :
self . order_by += ordering
else :
self . default_ordering = False
Из приведенного выше кода видно, что запрос выполняется только в том случае, если параметр соответствует ? или начинается с - и за ним следуют обычные символы или знак ..
Поэтому, когда имя столбца является UUID, оно становится недопустимым значением и не может быть передано в order_by.
Изменение кода обработки было принято и изменено следующим образом:
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Для проверки входных данных использовалась функция self.name_to_path.
Но после проверки, если в элементе содержится ., он считается запросом с именем таблицы, и выполняется команда continue, что приводит к прямому пропуску использования функции self.name_to_path для проверки допустимости данных.
Код обработки точки . в функции get_order_by выглядит следующим образом:
django/db/models/sql/compiler.py
if '.' in field :
table , col = col . split ( '.' , 1 )
order_by . append ((
OrderBy (
RawSQL ( ' %s . %s ' % (
self . quote_name_unless_alias ( table ), col ), [ ]),
descending = descending
), False ))
continue
Функция self.quote_name_unless_alias обрабатывает имя таблицы, фильтрует допустимые имена таблиц и пропускает фильтрацию имени столбца, поэтому мы можем вставить оператор SQL-инъекции.
В текущей версии Django 4.0 запрос по имени таблицы с помощью точки . был удален и больше не поддерживается; патч выпущен для версий 3.1 и 3.2. Затронуты версии 3.2 -> 3.2.4 и 3.1 -> 3.1.12.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
Исправление очень простое: была возвращена старая проверка данных с помощью ReGex.

Обновите Django до незатронутой версии.
Docker & Docker-compose
git clone https://github.com/WynSon/CVE-2021-35042.git./setup.sh для первоначальной настройкиsudo docker-compose up --buildsudo docker exec -it cve-2021-35042_web_1 python manage.py makemigrations cve202135042sudo docker exec -it cve-2021-35042_web_1 python manage.py migratehttp://localhost:8000/wolves/?order_by=nameЭкран после завершения установки

Условие: Для успешной эксплуатации мы обязательно должны знать имя таблицы каким-либо способом :))
При инъекции оператора мы должны знать имя таблицы, чтобы иметь возможность выполнить SQLi-оператор.
При вводе неправильного имени таблицы

При вводе правильного имени таблицы запрос orderby выполняется нормально.

Оператор в этом случае будет следующим:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
В этот момент мы можем завершить предыдущий оператор order_by и вставить SQL-оператор для эксплуатации.

SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name"); SELECT * from cve202135042_wolf where id =1; --) ASC