Caso práctico: vulnerabilidad de ejecución remota de código (RCE) en Celery
Calum Hutton
15 de febrero de 2022
0 minutos de lecturaDescripción general
Realicé una investigación basada en vulnerabilidades existentes de Python e identifiqué un patrón de software común entre ellas. Al aprovechar la potencia de nuestro motor interno de análisis estático, que también impulsa Snyk Code, nuestro producto de pruebas de seguridad de aplicaciones estáticas (SAST), pude crear reglas personalizadas y buscar en un gran conjunto de datos de código abierto para identificar otros proyectos que usaban el mismo patrón. Esto llevó al descubrimiento de una vulnerabilidad de inyección de comandos persistente en Celery. Las herramientas SAST, como Snyk Code, permiten a los desarrolladores identificar errores en su software únicamente mediante el análisis del código fuente estático y la detección de patrones de código ineficiente o peligroso.
Motivación
Me motivó a realizar este proyecto de investigación mi experiencia personal y la investigación de vulnerabilidades en proyectos de código abierto de Python (CVE-2017-11610 y CVE-2021-32807). Mi hipótesis era que la navegación entre objetos (la capacidad de obtener una referencia a un atributo arbitrario de un objeto desde otro objeto) es una característica común en Python. Quería investigar y demostrar esta hipótesis para identificar la prevalencia de este patrón en el ecosistema de Python en general, y en particular, encontrar casos de navegación entre objetos arbitraria (o casi arbitraria) que pudieran provocar vulnerabilidades de seguridad.
Contexto
En Python, casi todos los elementos del lenguaje son objetos, con sus propios atributos y métodos explícitos y heredados (incluidas las instancias de clases y los módulos). Por eso, las aplicaciones de Python pueden ofrecer un método para navegar por el espacio de nombres de un objeto y obtener una referencia a sus atributos o subatributos. Para hacerlo, se puede realizar una búsqueda recursiva de atributos.
El siguiente ejemplo simplificado de código muestra cómo se puede navegar por los espacios de nombres de los objetos en Python 3. Al importar un módulo inofensivo (random), es posible obtener una referencia a una función peligrosa (os.system) mediante getattr para obtener una referencia al módulo os importado (con el alias _os):
Si una aplicación de Python expusiera al usuario una funcionalidad de navegación entre objetos que le permitiera manipular la ruta solicitada del espacio de nombres del objeto o el atributo, esto podría dar lugar a una navegación entre objetos arbitraria (o casi arbitraria) y varios problemas potenciales:
El impacto más probable de exponer la funcionalidad de navegación entre objetos es que se vulneren los controles de acceso o se divulgue información, si un usuario puede obtener una referencia a un método o atributo privado, como
obj._private_method() or obj._secret.También es posible la ejecución remota de código (RCE) si se obtiene un método arbitrario, como
os.system(), y se invoca con argumentos proporcionados por el usuario. Esto es menos probable, ya que el usuario tendría que obtener una referencia al método y poder pasarle argumentos.
En resumen, un atacante que pueda controlar o manipular la navegación entre objetos en una aplicación de Python podría acceder a los atributos y subatributos de un módulo o una instancia de clase determinados y aprovechar su funcionalidad de forma irrestricta o inesperada.
El patrón vulnerable
El patrón identificado como relevante para los CVE mencionados es una búsqueda recursiva de atributos, generalmente basada en una ruta de Python delimitada por puntos. La ruta se divide y se recorre en un bucle; el elemento actual de la ruta se usa para obtener una referencia en el contexto mediante getattr(). Luego, la referencia de la llamada anterior a getattr() se usa como contexto para la siguiente llamada, y el proceso se repite hasta que no quedan elementos en la ruta. El siguiente script muestra cómo funciona:
La ruta separada por puntos se divide en dos cadenas (_os y system). En la primera iteración del bucle, la variable cls es una referencia al módulo random, por lo que getattr() busca el atributo _os del módulo random, que es el módulo os. El contexto de getattr() pasa a ser el módulo os y, en la siguiente iteración, se obtiene el atributo system del módulo os, es decir, os.system().
Caso práctico (Celery - CVE-2021-23727)
Usé el motor de Snyk Code para desarrollar reglas que identificaran el mismo patrón en otros proyectos de código abierto de Python. Una de las coincidencias de regla y patrón se encontró en Celery, una cola de tareas asíncronas de código abierto basada en el paso de mensajes distribuidos. La regla coincidió con la función exception_to_python dentro de la clase celery.backends.base.Backend:

El patrón recursivo de getattr aparece dentro de la función, en la línea 354. Tras revisar más a fondo la función y su uso en Celery, quedó claro que todo el objeto dict exc provenía de datos JSON almacenados en un backend de Celery. Por lo tanto, un usuario con acceso al servidor del backend podría controlarlo o manipularlo, y todos los campos del dict deberían considerarse potencialmente contaminados.
Un análisis más detallado del código permitió determinar que se accede a una referencia de módulo arbitraria según una propiedad del objeto dict exc (línea 352). Luego, se usa el patrón recursivo de atributos para buscar un atributo arbitrario del módulo. A continuación, se invoca la referencia al atributo obtenida con un único argumento de cadena, que también se toma del objeto dict exc (línea 364).
Dado que todo el objeto dict exc podría estar contaminado y que no hay validaciones para impedir el acceso a módulos y atributos arbitrarios, se identificó que este código probablemente era vulnerable a la inyección de comandos persistente.
Comprobé esta hipótesis con la consola de Python: importé las clases pertinentes y preparé un dict malicioso para simular a un atacante que crea un blob JSON malicioso en la base de datos de un backend de Celery:
Primero, creo un dict de Python para pasarlo a la función vulnerable. Este dict contiene propiedades que controlan el módulo al que se accede (exc_module), el atributo del módulo del que se obtiene una referencia (exc_type) y, por último, el argumento que se pasa al método obtenido (exc_message).
Las siguientes líneas importan el código vulnerable de Celery en la consola de Python e inicializan la clase Backend con un objeto Celery.
Por último, la vulnerabilidad se activa cuando se pasa el dict al método vulnerable exception_to_python.
La última línea del fragmento de código anterior muestra el resultado del comando id, lo que demuestra que el dict preparado se deserializó y activó correctamente una inyección de comandos arbitraria dentro de la clase Backend.
En un sistema que utiliza Celery, un atacante que aprovechara esta vulnerabilidad podría inyectar comandos en el productor y, potencialmente, tomar el control total del sistema. Si el backend de Celery es remoto, esta vulnerabilidad también podría permitir que los atacantes que obtuvieron acceso al backend se desplazaran lateralmente por la red de una organización y consiguieran establecerse en más infraestructura de red.
Corrección
Este problema se comunicó de manera responsable a Celery y se corrigió en la versión 5.2.2 del software. La corrección agregó validaciones del módulo de destino y comprobó los tipos de los atributos resueltos antes de invocar funciones arbitrarias con entradas potencialmente contaminadas. En general, al deserializar objetos, siempre se debe tener cuidado con cualquier dato potencialmente contaminado, incluso si proviene de una fuente de datos remota o de una base de datos. Si es posible, se deben comprobar y validar los tipos de los objetos antes de deserializarlos o, al menos, antes de invocar un método o una propiedad del objeto deserializado.
Encontrar esta vulnerabilidad de inyección de comandos en Celery y realizar el análisis necesario de los datos contaminados fue más sencillo con Snyk Code. Nuestra revolucionaria herramienta SAST es una alternativa rápida, precisa y fácil de usar para desarrolladores frente a las herramientas de seguridad tradicionales. Los complementos para IDE integran pruebas en tiempo real en los flujos de trabajo existentes y permiten a los desarrolladores encontrar y corregir vulnerabilidades en tan solo 5 minutos. Inicia hoy una prueba gratis y descubre cómo Snyk Code simplifica el desarrollo seguro.
Comienza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.
Referencias
Celery (CVE-2021-23727): https://security.snyk.io/vuln/SNYK-PYTHON-CELERY-2314953
