
Vulnerabilidade de injeção SQL no Django
O Django é um framework de aplicação web de código aberto, escrito em Python, construído com base no modelo MVC (Model-View-Controller). Foi originalmente desenvolvido para gerenciar sites de conteúdo de notícias pertencentes ao grupo editorial Lawrence, como um sistema de gerenciamento de conteúdo (CMS).
As versões 3.1.x a 3.1.13 e 3.2.x a 3.2.5 do Django possuem uma vulnerabilidade de injeção SQL.
A causa dessa vulnerabilidade é que a função de filtragem dos dados de entrada controlados pelo usuário no QuerySet.order_by() não é suficiente para prevenir ataques de injeção SQL. Essa vulnerabilidade pode ser explorada para permitir que um atacante realize ações não autorizadas, levando ao vazamento de dados sensíveis.
| CVE-ID | CVE-2021-35042 |
|---|
| Severidade | 9.8 - CRÍTICA |
| CWE-ID | CWE-89: Neutralização incorreta de elementos especiais usados em um comando SQL ('Injeção SQL') |
| Data de publicação da vulnerabilidade | 01/07/2021 |
| Software afetado | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Requer autenticação | Não é necessária |
No Django, a criação de tabelas e definição de campos no banco de dados é feita declarando uma classe de modelo no arquivo models.py. Neste exemplo, declaramos uma tabela chamada Wolf e um campo chamado name.


O framework ORM integrado ao Django é usado para manipular o banco de dados, e o resultado de uma consulta é um conjunto, chamado de QuerySet.
order_by(campos)
Por padrão, order_by() retorna um QuerySet ordenado de acordo com a ordem especificada na opção ordering do Meta do Model. Podemos sobrescrever a condição order_by em cada consulta usando o método order_by().
Exemplo
wolves = Wolf.objects.order_by('-name', 'id')
O resultado da consulta acima será ordenado de forma decrescente pelo campo name e depois de forma crescente pelo id. O sinal negativo antes do nome do campo name indica uma ordenação decrescente.
O exemplo a seguir ordena o resultado retornado pelo campo recebido do usuário; se nenhum valor for passado, ordena pelo campo
id.
Resultado

Nas versões 3.1 e 3.2, o Django permite combinar o método de consulta com o nome da tabela na consulta order_by. Essa é a principal causa dessa vulnerabilidade.
Passar um nome de tabela nos dá o mesmo resultado que passar um nome de campo normalmente.
cve202135042_wolf é o nome da tabela
Primeiro, a aplicação chama diretamente a função order_by(). O código que processa a função order_by() está definido em:
django/db/models/query.py

A função
order_by()realiza duas ações:
- Remove todas as ordenações atuais que estão sendo chamadas por
order_by()e remove o parâmetro padrão passado quandoorder_byrecebe um valor diferente.- Passa os parâmetros para
order_by. A funçãoadd_ordering()realiza essa etapa.
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
O parâmetro passado para add_ordering() é um array.
Exemplo: quando o parâmetro é passado da seguinte forma:
wolves = Wolf.objects.order_by( 'name' , 'id' )A aplicação converte isso em uma consulta SQL no banco de dados:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
Quando recebido, a função add_ordering verifica cada elemento do array; se for uma string, ela é verificada em 5 casos:
if '.' in item:Verifica se é uma consulta com um nome de coluna e se essa coluna tem um nome de tabela especificado na instrução SQL. Se sim, emite um aviso econtinue.if item == '?':Se o valor do elemento for '?', o resultado da saída será ordenado aleatoriamente,continue.if item.startswith('-'):Se o item começar com '-', o resultado da consulta será ordenado DESC (decrescente).if item in self.annotations:Verifica se contém um comentário; se sim,continue.if self.extra and item in self.extra:Verifica se há uma cláusula extra; se sim,continue.
Após as 5 verificações, o parâmetro é passado para a função self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) para verificar se é um nome de coluna válido. Depois, se for válido, é adicionado ao self.ordering da classe Query para processamento.
O ORM do Django realiza a filtragem dos dados inseridos nas consultas de maneira muito rigorosa, mas a mudança no código que levou a essa injeção SQL ocorreu porque o autor assumiu que, se o nome da coluna fosse um UUID (Identificador Único Universal), a consulta order_by não poderia ser executada.
Ou seja, se os dados de entrada estivessem no formato xxx-xxx-xxx-xxx (formato UUID), a consulta não poderia ser executada.
Código antes da alteração
# 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
Pelo código acima, podemos ver que se o parâmetro corresponder a ? ou começar com - seguido de caracteres comuns ou ponto final, a consulta é executada.
Portanto, quando o nome da coluna é um UUID, ele se torna um valor inválido e não pode ser usado em order_by.
A alteração do código de tratamento foi aceita e ficou assim:
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Ele passou a usar a função self.name_to_path para validar a entrada.
Mas, após verificar se há . no item, ele considera que é uma consulta com nome de tabela; o comando continue é executado, ignorando diretamente o uso da função self.name_to_path para verificar a validade dos dados.
O código que trata o ponto na função 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
A função self.quote_name_unless_alias trata o nome da tabela, filtra nomes de tabela válidos e ignora a filtragem do nome da coluna; portanto, podemos inserir um comando de injeção SQL.
Na versão atual do Django 4.0, a consulta por nome de tabela usando . foi removida e não é mais suportada. A correção foi aplicada nas versões 3.1 e 3.2. As versões 3.2.x a 3.2.4 e 3.1.x a 3.1.12 são afetadas.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
A modificação é simples: a verificação dos dados via Regex antiga foi reintroduzida.

Atualizar o Django para uma versão não afetada.
Docker & Docker-compose
git clone https://github.com/LUUANHDUC/CVE-2021-35042.git./setup.sh para a configuração inicialsudo 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=nameTela após a instalação

Condição: Para explorar, precisamos saber o nome da tabela de alguma forma :))
Ao injetar um comando, precisamos saber o nome da tabela para conseguir executar o SQLi.
Quando o nome da tabela está errado

Quando o nome da tabela está correto, a consulta orderby é executada normalmente.

A instrução neste momento se torna
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
Neste ponto, podemos encerrar a instrução order_by anterior e inserir um comando SQL para exploração.

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