Los errores detrás de las vulnerabilidades - Parte 3
Esta es la tercera parte de nuestra serie de blogs de cinco partes que explora los errores de código que conducen a las vulnerabilidades que aparecen todos los días. En esta parte, cubriremos los bugs #15 a #11 del Mitre CWE Top 25 para 2022, incluyendo el favorito de todos, "NULL pointer dereference", en el #11.
Puede encontrar parte 1 aquí y parte 2 aquí.
15 - Uso de credenciales codificadas
Los problemas clasificados bajo esta clase de vulnerabilidad se originan al incluir credenciales en un paquete compilado, o al incluirlas en el medio de distribución o dispositivo de almacenamiento que llega al usuario final. Esto significa que, si se descubren, esas credenciales funcionarían en todos los mismos dispositivos. Más que un error de codificación propiamente dicho, se trata más bien de un problema de empaquetado y liberación.
Hay muchas maneras de que esto ocurra - como el uso de credenciales de prueba codificadas durante CI/CD que llegan al entregable al final del proceso (en lugar de utilizar tokens de autenticación dedicados, por ejemplo), o incluir alguna forma de puerta trasera a un dispositivo de producción que llega al mercado.
Una vez detectado, este tipo de problema puede ser muy difícil de resolver. Cuando ocurre en un dispositivo de hardware, la única forma de eliminar una credencial codificada es actualizar el firmware. La dificultad de esta operación dependerá del dispositivo concreto.
Pero esto también puede ocurrir, y de hecho ocurre, en los paquetes de software, no sólo en el hardware. En este caso, como la credencial está integrada en el propio paquete de software, suele ser necesario actualizar la versión para solucionar el problema.
También es relativamente fácil detectar este tipo de credenciales al realizar ingeniería inversa de software o hardware, por lo que los actores maliciosos disfrutan mucho descubriéndolas durante sus operaciones.
El verdadero problema con las credenciales hard-coded es que anularán cualquier restricción de acceso que puedas establecer en un sistema dado. Si existe una credencial de administrador codificada, no importa si no tienes otras cuentas administrativas creadas: la codificada podrá entrar.
A nivel de arquitectura, las credenciales codificadas también aparecen al configurar conexiones entre sistemas, donde una cadena de conexión o una contraseña se almacenan en un archivo de configuración que se lee para establecer la conexión. Un atacante que encontrara ese archivo podría suplantar la conexión.
14 - Autenticación incorrecta
La autenticación incorrecta describe una situación en la que, en lugar de validar un conjunto de credenciales, tokens u otro mecanismo de autenticación, una aplicación confía en un cliente que afirma tener una identidad determinada.
Esto es muy común cuando se trata de conexiones a sitios web que validan la presencia de una cookie en lugar de validar la autenticación al sitio. Se puede falsificar una cookie para afirmar que ya ha sido autenticada, y el sitio web confiará en ella.
Otro escenario común es cuando la autenticación se valida sólo en el lado del cliente, lo que permite a un atacante modificar o interferir con el software del cliente y obtener acceso a los recursos del lado del servidor alegando que ya está autenticado.
13 - Desbordamiento o desbordamiento de enteros
Se trata de una de las primeras formas de errores de software. En una arquitectura de ordenador determinada (32/64 bits, ARM, mips, etc), cualquier variable ocupará una cantidad determinada de memoria. Cuanto mayor sea la cantidad de memoria, mayor será el valor que puede almacenarse en esa variable. Sin embargo, cuando se realizan operaciones aritméticas, es fácil no hacer una comprobación para ver si el resultado de dicha operación todavía cabe en el tamaño de memoria de la variable - como, por ejemplo, multiplicar dos enteros juntos y almacenar el resultado podría conducir a un valor tan grande que ya no cabe en el tamaño máximo de la variable. Cuando esto ocurre, el entero se "desborda".
Las diferentes arquitecturas informáticas tratan este evento de diferentes maneras: algunas descartan el valor extra, lo que resulta en el almacenamiento de un valor incorrecto; otras escriben el valor en la siguiente posición de memoria disponible, lo que potencialmente sobrescribe algo ya almacenado allí; y otras envuelven el valor - como el contador de kilometraje en un coche que vuelve a cero cuando alcanza el valor máximo, y usted continúa conduciendo.
Imaginando que la variable controla el número de iteraciones en un bucle, puede conducir a un bucle infinito. En otros escenarios, puede causar un fallo, una aplicación que no responde o un servicio que no responde, y también podría agotar la memoria disponible o la inestabilidad general del sistema. También puede conducir directamente a escenarios de desbordamiento de búfer (cubierto en otro bug de esta lista).
Dado que muchos lenguajes informáticos no incluyen comprobaciones de límites por defecto -y tienen que habilitarse explícitamente mediante banderas del compilador o utilizando bibliotecas específicas-, es importante tomar una decisión informada sobre el lenguaje a utilizar para una aplicación que se esté planificando.
Si esto ocurre en una aplicación ya desarrollada, es necesario añadir comprobaciones de límites adecuadas para validar que los cálculos nunca quedarán fuera de los tamaños de memoria variable disponibles, recordando siempre que, si una aplicación es multiplataforma, las diferentes plataformas/arquitecturas tendrán diferentes tamaños permitidos.
Herramientas como los fuzzers suelen desencadenarlos y, como tales, pueden ser de gran valor para los desarrolladores.
Encontrar situaciones en el flujo de una aplicación que sean vulnerables a este tipo de bugs es una de las tareas más básicas que realiza un atacante.
12 - Deserialización de datos no fiables
La serialización (o marshaling) es el proceso de convertir un dato almacenado en memoria en un formato que pueda almacenarse o enviarse (a través de una conexión de red) a un sistema/aplicación/componente diferente. La deserialización (o unmarshaling) es el proceso inverso, en el que un flujo de datos se convierte en una representación en memoria de un objeto interno de una aplicación ("objeto" significa variable o agrupación de variables).
Cuando una aplicación confía ciegamente en que los datos que recibe (ya sean leídos de un archivo, aceptados de un formulario en un sitio web o a través de una conexión de red) son intrínsecamente válidos e intenta deserializarlos en una ubicación de memoria determinada, se expone a un riesgo. Los flujos de datos especialmente diseñados pueden engañar a una aplicación para que sobrescriba ubicaciones de memoria existentes fuera de la ubicación prevista o no proporcione datos suficientes para llenar todas las variables, lo que puede dar lugar a variables no inicializadas o incluso a la entrega de datos fuera de los límites esperados, todo lo cual puede conducir a eventos imprevistos.
Como regla general de desarrollo, nunca se debe confiar en ninguna entrada, sin excepciones. Incluso si el desarrollador está escribiendo el cliente y la aplicación del servidor por sí mismo, siempre es posible subvertir una conexión, o intentar manipular la comunicación entre ellos, o hacerse pasar por un lado de la conversación, lo que lleva a un escenario en el que los datos que se aceptan ya no son de confianza, y por lo tanto no deben ser deserializados.
El siguiente código Java parece bastante benigno:
try { File file = new File("object.obj"); ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); javax.swing.JButton button = (javax.swing.JButton) in.readObject(); in.close(); } [source: https://cwe.mitre.org/data/definitions/502.html] |
Pero, si se examina más detenidamente, el archivo "object.obj" no está validado en absoluto, y podría contener algo distinto de una definición de botón, como espera la aplicación.
11 - Desviación de puntero nulo
Esta es una de las fuentes más comunes de vulnerabilidades que se pueden encontrar en cualquier aplicación escrita en un lenguaje amigable con los punteros, como C. Consiste en intentar obtener el valor referenciado por un puntero que ha sido invalidado previamente o que aún no ha sido inicializado.
El resultado habitual, salvo diferencias arquitectónicas, es que la aplicación se bloquee, ya que el sistema operativo detectará el intento de leer una posición de memoria fuera del espacio de memoria permitido para esa aplicación.
El siguiente ejemplo en C# ilustra el problema:
string name; int k=0; if(k==1) { name=Console.ReadLine(); } Console.Writeline(name.Length); |
Cuando la ejecución llegue a la última llamada, el método "Length" será llamado sobre una cadena nula, colapsando la aplicación.
Es interesante observar que muchos lenguajes han introducido cambios (tipos anulables, indicadores del compilador, nuevos tipos de advertencia) dirigidos específicamente a detectar este tipo de error en una fase temprana del proceso de desarrollo. Otros renuncian totalmente al uso explícito de punteros para evitarlo, pero aparte de unas prácticas de programación cautelosas, no hay ninguna bala de plata que sea 100% efectiva para encontrar y prevenir las desreferencias de punteros NULL.
Situaciones como la descrita en el bug #12, pueden llevar a que se introduzcan valores inesperados en el flujo de la aplicación, provocando desferencias de punteros NULL.
Una solución relativamente sencilla a este error es anteponer siempre a cualquier uso adecuado de una variable una comprobación para validar que no es NULL, pero la programación concurrente/paralela puede hacer que esa sea una solución menos óptima.