
Уязвимость 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 |
|---|---|
| Критичность | 9.8 - КРИТИЧЕСКИЙ |
| CWE - ID | CWE-89: Неправильная нейтрализация специальных элементов, используемых в SQL-команде ('SQL-инъекция') |
| Дата публикации уязвимости | 1/7/2021 |
| Затронутое ПО | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Требуется аутентификация | Не требуется |
В 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, он проверяется в следующих пяти случаях:
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.
После пяти проверок параметр передаётся в функцию 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…
3.1.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
Исправление очень простое: возвращена прежняя проверка данных с помощью ReGex

Обновите Django до незатронутой версии.
Docker & Docker-compose
git clone https://github.com/LUUANHDUC/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Экран после завершения установки

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

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

Запрос в этом случае будет таким:
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
https://www.djangoproject.com/weblog/2021/jul/01/security-releases/ https://xz.aliyun.com/t/9834 https://www.bugxss.com/vulnerability-report/3095.html https://blankheart.top/2022/04/07/cve-2021-35042/ https://itcn.blog/p/1648921763575859.html https://github.com/YouGina/CVE-2021-35042