
Une analyse de base sur CVE-2021-35942. Injection SQL dans Django.
Django est un framework de développement d'applications web open source, écrit en Python et construit selon le modèle MVC (Model - View - Controller). Il a été initialement conçu pour gérer les sites web de contenu d'actualités appartenant au groupe de presse Lawrence, en tant que logiciel CMS (Content Management System).
Les versions de Django 3.1.x -> 3.1.13 et 3.2.x -> 3.2.5 présentent une vulnérabilité d'injection SQL.
La cause de cette vulnérabilité est que la fonction de filtrage des données d'entrée contrôlées par l'utilisateur dans QuerySet.order_by() n'est pas suffisante pour se prémunir contre les attaques par injection SQL. Cette vulnérabilité peut être exploitée pour permettre à un attaquant d'effectuer des actions non autorisées, conduisant à la fuite de données sensibles.
| CVE - ID | CVE-2021-35042 |
|---|
| Sévérité | 9.8 - CRITIQUE |
| CWE - ID | CWE-89 : Neutralisation incorrecte d'éléments spéciaux utilisés dans une commande SQL ('Injection SQL') |
| Date de publication de la vulnérabilité | 01/07/2021 |
| Logiciels concernés | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Authentification requise | Non requise |
Dans Django, la création de tables et la définition des champs dans la base de données s'effectuent en déclarant une classe de modèle dans le fichier models.py. Dans cet exemple, nous déclarons une table nommée Wolf et un champ nommé name.


Le framework ORM intégré à Django est utilisé pour interagir avec la base de données, et le résultat d'une requête est un ensemble, appelé QuerySet.
order_by(fields)
Par défaut, order_by() renvoie un QuerySet trié selon un ordre spécifié dans l'option ordering du Meta du modèle. Nous pouvons remplacer la condition order_by dans chaque requête en utilisant la méthode order_by().
Exemple
wolves = Wolf.objects.order_by('-name', 'id')
Le résultat de la requête ci-dessus sera trié par ordre décroissant selon le champ name, puis par ordre croissant selon id. Le signe négatif devant le nom du champ name indique que le résultat est trié par ordre décroissant.
L'exemple suivant trie le résultat renvoyé selon le champ reçu de l'utilisateur ; si aucune valeur n'est transmise, le tri s'effectue selon le champ
id.
Résultat

Dans les versions 3.1 et 3.2, Django permet de combiner la méthode de requête avec le nom de la table dans la requête order_by. C'est également la cause principale de cette vulnérabilité.
Le fait de transmettre un nom de table nous donne le même résultat que la transmission d'un nom de champ normalement
cve202135042_wolf est le nom de la table
Tout d'abord, l'application appelle directement la fonction order_by() ; le code traitant la fonction order_by() est défini à :
django/db/models/query.py

La fonction order_by() effectue deux opérations
- Supprime toutes les méthodes actuellement appelées par order_by() et supprime le paramètre par défaut transmis lorsque order_by reçoit une valeur différente.
- Transmet le paramètre à order_by. La fonction
add_ordering()effectue cette opération.
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
Le paramètre transmis à add_ordering() est un tableau.
Par exemple, lorsque le paramètre est transmis comme suit :
wolves = Wolf.objects.order_by( 'name' , 'id' )L'application convertit alors cela en une requête dans la base de données comme suit :
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
Lorsqu'il est transmis, la fonction add_ordering vérifie chaque élément du tableau ; s'il s'agit d'une string, elle est vérifiée selon les 5 cas suivants :
if '.' in item:Elle vérifie s'il s'agit d'une requête avec un nom de colonne et si cette colonne possède un nom de table spécifié dans la commande SQL. Si c'est le cas, un avertissement est émis etcontinueest exécuté.if item == '?':Si la valeur de l'élément est le signe '?', le résultat de sortie est trié aléatoirement, puiscontinue.if item.startswith('-'):Si l'élément commence par le caractère '-', le résultat de la requête est trié par ordre décroissant (DESC).if item in self.annotations:Elle vérifie s'il contient un commentaire ; si c'est le cas,continue.if self.extra and item in self.extra:Détermine s'il y a des ajouts supplémentaires et, si c'est le cas,continue.
Après ces 5 vérifications, le paramètre est ensuite transmis à la fonction self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) pour continuer à vérifier s'il s'agit d'un nom de colonne valide ; ensuite, s'il est valide, il est ajouté à self.ordering de la classe Query pour un traitement ultérieur.
L'ORM de Django filtre très strictement les données insérées dans la requête, mais cette modification du code source conduisant à l'injection SQL est due au fait que l'auteur a émis l'hypothèse que si le nom de colonne est une colonne UUID (Universal Unique Identifier), la requête order_by ne pourrait pas être exécutée.
Cela signifie que si les données transmises sont au format xxx-xxx-xxx-xxx (format UUID), la requête ne peut pas être exécutée.
Code avant la modification
# 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
À partir du code ci-dessus, nous pouvons voir que si le paramètre correspond à ? ou commence par - suivi de caractères ou de points . ordinaires, la requête est alors exécutée.
Par conséquent, lorsque le nom de colonne est un UUID, il s'agit d'une valeur invalide qui ne peut pas être insérée dans order_by.
La modification du code de ce traitement a été acceptée et modifiée comme suit :
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Elle utilise la fonction self.name_to_path pour valider les données d'entrée.
Mais après vérification, si . est présent dans l'élément, il est considéré comme une requête avec un nom de table ; la commande continue est exécutée, ce qui conduit à ignorer directement l'utilisation de la fonction self.name_to_path pour vérifier la validité des données.
Le code traitant le point . dans la fonction get_order_by est le suivant :
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 fonction self.quote_name_unless_alias traite le nom de la table, filtre les noms de tables valides et ignore le filtrage du nom de colonne ; nous pouvons donc insérer une commande d'injection SQL.
Dans la version actuelle de Django 4.0, la requête par nom de table utilisant le point . a été supprimée et n'est plus prise en charge ; le correctif a été publié pour les versions 3.1 et 3.2. Les versions 3.2 -> 3.2.4 et 3.1 -> 3.1.12 sont concernées.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
La modification est très simple : la vérification des données par l'ancienne ReGex a été rétablie.

Mettre à jour Django vers une version non concernée.
Docker & Docker-compose
git clone https://github.com/WynSon/CVE-2021-35042.git./setup.sh pour l'installation initialesudo 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Écran après l'installation terminée

Condition : Pour pouvoir exploiter la vulnérabilité, nous devons obligatoirement connaître le nom de la table d'une manière ou d'une autre :))
Lors de l'injection de la commande, nous devons connaître le nom de la table pour pouvoir exécuter la commande SQLi.
Lorsque le nom de la table est saisi incorrectement

Lorsque le nom de la table est correct, la requête orderby s'exécute normalement.

La commande devient alors
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
À ce stade, nous pouvons terminer la commande order_by précédente et insérer une commande SQL pour exploiter la vulnérabilité.

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