
Vulnerabilidad de inyección SQL en Django
Django es un framework de aplicaciones web de código abierto, escrito en Python, construido según el modelo MVC (Modelo - Vista - Controlador). Originalmente fue construido para gestionar sitios web de contenido de noticias propiedad de la corporación editorial Lawrence, software CMS (Sistema de gestión de contenidos).
Django versiones 3.1.x -> 3.1.13 y versiones 3.2.x -> 3.2.5 contienen una vulnerabilidad de inyección SQL.
La causa de esta vulnerabilidad es que la función de filtrado de los datos de entrada controlados por el usuario en QuerySet.order_by() no es suficiente para prevenir ataques de inyección SQL. Esta vulnerabilidad puede ser explotada para permitir a un atacante realizar acciones no autorizadas que conduzcan a la filtración de datos confidenciales.
| CVE - ID | CVE-2021-35042 |
|---|---|
| Severidad | 9.8 - CRÍTICA |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| Fecha de publicación de la vulnerabilidad | 1/7/2021 |
| Software afectado | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Requiere autenticación | No requerida |
En Django, la creación de tablas y la definición de los campos en la base de datos se realiza declarando una clase modelo en el archivo models.py. En este ejemplo, declaramos una tabla llamada Wolf y un campo llamado name.


El framework ORM integrado en Django se utiliza para manipular la base de datos, y el resultado de la consulta es un conjunto; este conjunto es un QuerySet.
order_by(fields)
Por defecto, order_by() devuelve un QuerySet ordenado según un orden especificado en la opción ordering en el Meta del modelo. Podemos sobrescribir la condición de order_by en cada consulta usando el método order_by().
Ejemplo
wolves = Wolf.objects.order_by('-name', 'id')
El resultado de la consulta anterior se ordenará de forma descendente por el campo name, y luego de forma ascendente por id. El signo negativo delante del nombre del campo name indica que los resultados se ordenan de forma descendente.
El siguiente ejemplo ordenará los resultados devueltos según el campo recibido del usuario; si no se pasa ningún valor, se ordenará por el campo
id.
Resultado

En las versiones 3.1 y 3.2, Django permite combinar el método de consulta con el nombre de la tabla en la consulta order_by. Esta es también la causa principal de esta vulnerabilidad.
Pasar un nombre de tabla nos da el mismo resultado que pasar un nombre de campo normalmente
cve202135042_wolf es el nombre de la tabla
Primero, la aplicación llama directamente a la función order_by(); el código que maneja la función order _by() está definido en:
django/db/models/query.py

La función order_by() realiza 2 tareas
- Elimina todos los métodos actuales que están siendo llamados por order_by() y elimina el parámetro predeterminado que se pasa cuando order_by recibe un valor diferente.
- Pasa los parámetros a order_by. La función
add_ordering()realiza esta tarea.
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
El parámetro pasado a add_ordering() es un array.
Por ejemplo, cuando el parámetro se pasa de la siguiente manera:
wolves = Wolf.objects.order_by( 'name' , 'id' )Entonces, la aplicación lo convierte en la siguiente consulta en la base de datos:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
Cuando se pasa, la función add_ordering revisa cada elemento del array; si es un string, se comprueban los siguientes 5 casos:
if '.' in item:Comprueba si se trata de una consulta con nombre de columna y si esa columna tiene un nombre de tabla especificado en la sentencia SQL. Si es así, muestra una advertencia ycontinue.if item == '?':Si el valor del elemento es el signo '?', el resultado de la salida se ordena aleatoriamente;continue.if item.startswith('-'):Si el elemento comienza con el carácter '-', el resultado de la consulta se ordena DESC (descendente).if item in self.annotations:Comprueba si contiene un comentario; si es así,continue.if self.extra and item in self.extra:Determina si hay una adición extra y, si la hay,continue.
Después de las 5 comprobaciones, el parámetro se pasa a la función self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) para seguir verificando si es un nombre de columna válido; luego, si es válido, se añade a self.ordering de la clase Query para su posterior procesamiento.
El ORM de Django filtra los datos introducidos en la consulta de forma muy estricta, pero este cambio de código que provoca la inyección SQL se debe a que el autor asumió que si un nombre de columna era una columna UUID (Identificador Único Universal), la consulta order_by no podría ejecutarse.
Es decir, si los datos introducidos son xxx-xxx-xxx-xxx (formato UUID), la consulta no puede ejecutarse.
Código antes del cambio
# 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
Del código anterior, podemos ver que si el parámetro coincide con ? o comienza con - y va seguido de caracteres normales o un punto ., la consulta se ejecuta.
Por lo tanto, cuando el nombre de la columna es un UUID, es un valor no válido y no se puede pasar a order_by.
Este cambio en el código de procesamiento fue aceptado y se modificó de la siguiente manera:
https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

Usa la función self.name_to_path para validar los datos de entrada.
Pero después de comprobar si hay un . en el elemento, lo trata como una consulta con nombre de tabla; la instrucción continue se ejecuta, lo que lleva a omitir directamente el uso de la función self.name_to_path para verificar la validez de los datos.
El código que maneja el punto . en la función get_order_by es el siguiente:
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 función self.quote_name_unless_alias procesa el nombre de la tabla, filtra los nombres de tabla válidos y omite el filtrado del nombre de la columna, por lo que podemos inyectar una sentencia SQL.
En la versión actual de Django 4.0, la consulta por nombre de tabla usando el punto . ha sido eliminada y ya no se admite; el parche se publicó para las versiones 3.1 y 3.2. Las versiones 3.2 -> 3.2.4 y 3.1 -> 3.1.12 están afectadas.
3.2.x Corregido CVE-2021-35042 -- Se previno la inyección SQL en QuerySet.o…
3.1.x Corregido CVE-2021-35042 -- Se previno la inyección SQL en QuerySet.o…
La modificación es muy sencilla: se restauró la validación de datos con la antigua ReGex.

Actualice Django a una versión no afectada.
Docker & Docker-compose
git clone https://github.com/LUUANHDUC/CVE-2021-35042.git./setup.sh para la configuración 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=namePantalla después de completar la instalación

Condiciones: Para poder explotarla, debemos conocer el nombre de la tabla de alguna manera :))
Al inyectar la consulta, debemos conocer el nombre de la tabla para poder ejecutar la sentencia SQLi.
Al introducir un nombre de tabla incorrecto

Al introducir el nombre de tabla correcto, la consulta order_by se ejecuta normalmente.

La consulta ahora quedaría así:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
En este punto, podemos cerrar la consulta order_by anterior e insertar una sentencia SQL para explotarla.

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