
Vulnérabilité d'injection SQL dans Django
Django est un framework d'application Web open source, écrit en Python, 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 à la société d'édition Lawrence, un 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é pourrait être exploitée pour permettre à un attaquant d'effectuer des actions non autorisées, entraînant une 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 se font 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 sur le champ name, puis par ordre croissant sur id. Le signe négatif devant le nom du champ name indique un tri décroissant.
L'exemple suivant trie le résultat en fonction du champ reçu de l'utilisateur ; si aucune valeur n'est transmise, il trie par 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 clause order_by. C'est la principale cause de cette vulnérabilité.
Transmettre un nom de table donne le même résultat que transmettre un nom de champ normalement.
cve202135042_wolf est le nom de la table
D'abord, l'application appelle directement la fonction order_by(). Le code gérant la fonction order_by() est défini dans :
django/db/models/query.py

La fonction order_by() effectue deux actions :
- Supprime tous les tris actuels appelés par order_by() et supprime le paramètre par défaut transmis lorsque order_by reçoit une valeur différente.
- Transmet les paramètres à order_by. La fonction
add_ordering()exécute cette tâche.
def add_ordering(self, *ordering):
"""
Ajoute les éléments de la séquence 'ordering' à la clause "order by"
de la requête. Ces éléments sont soit des noms de champs (pas des noms de colonnes) --
éventuellement avec un préfixe de direction ('-' ou '?') -- soit des expressions OrderBy.
Si 'ordering' est vide, supprime tout tri de la requête.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passer des alias bruts de colonnes à order_by() est '
'obsolète. Enveloppez %r dans une expression RawSQL avant '
'de le passer à 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() valide la recherche. Un FieldError descriptif
# sera levé si ce n'est pas le cas.
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(
'Utiliser une fonction d'agrégation dans order_by() sans l\'inclure '
'également dans annotate() n\'est pas autorisé : %s' % item
)
if errors:
raise FieldError('Arguments order_by invalides : %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
Le paramètre transmis à add_ordering() est un tableau.
Exemple : lorsque le paramètre est transmis comme suit :
wolves = Wolf.objects.order_by( 'name' , 'id' )L'application transforme 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 ; si c'est une string, elle est vérifiée selon 5 cas :
if '.' in item:Vérifie s'il s'agit d'une requête avec un nom de colonne et que cette colonne a un nom de table spécifié dans l'instruction SQL. Si c'est le cas, un avertissement est émis etcontinueest exécuté.if item == '?':Si la valeur de l'élément est '?', le résultat de sortie est trié aléatoirement,continue.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:Vérifie s'il contient un commentaire ; si oui,continue.if self.extra and item in self.extra:Détermine s'il y a un supplément et si oui,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 effectue un filtrage très strict des données insérées dans la requête, mais cette modification du code source conduisant à l'injection SQL est due à l'hypothèse de l'auteur selon laquelle 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 xxx-xxx-xxx-xxx (format d'un 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
D'après le code ci-dessus, on voit que si le paramètre correspond à ? ou commence par - suivi de caractères ou de points ordinaires, alors la requête est exécutée.
Ainsi, lorsque le nom de colonne est un UUID, c'est une valeur invalide et ne peut pas être utilisée dans order_by.
La modification du code de traitement a été acceptée et a été modifiée comme suit :
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Il a utilisé 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 ; l'instruction continue est exécutée, ce qui ignore 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, permettant ainsi d'injecter une instruction 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. Des correctifs ont été publiés 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 validation des données par l'ancienne expression régulière a été réintroduite.

Mettre à jour Django vers une version non affectée.
Docker & Docker-compose
git clone https://github.com/LUUANHDUC/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 installation terminée

Condition : Pour pouvoir exploiter, nous devons impérativement connaître le nom de la table d'une manière ou d'une autre :))
Lors de l'injection de l'instruction, nous devons connaître le nom de la table pour pouvoir exécuter l'instruction SQLi.
En cas de nom de table incorrect

En cas de nom de table correct, la requête orderby s'exécute normalement.

L'instruction devient alors :
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
À ce stade, nous pouvons terminer la clause order_by précédente et insérer une instruction SQL pour l'exploitation.

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