Skip to main content

Exploración de 3 tipos de vulnerabilidades de directory traversal en C/C++

Escrito por
Headshot of Kirill Efimov

Kirill Efimov

blog feature security beta

4 de abril de 2022

0 minutos de lectura

Las vulnerabilidades de directory traversal (también conocidas como vulnerabilidades de recorrido de rutas) permiten que actores maliciosos accedan a carpetas a las que no deberían tener acceso. En esta publicación, veremos cómo funcionan las vulnerabilidades de directory traversal en servidores web escritos en C/C++ y cómo prevenirlas.

Para el equipo de Security Research, los ecosistemas de C y C++ estuvieron fuera de nuestro alcance durante mucho tiempo, pero eso cambió cuando FossID se unió a Snyk. La experiencia de FossID era en C/C++, lo que nos permitió iniciar un par de proyectos para entender la postura general de seguridad de la información de esos lenguajes. Como resultado, descubrimos muchas vulnerabilidades de alta gravedad y nos entusiasma empezar a compartir nuestros hallazgos.

En esta publicación, nos centraremos en la limitación incorrecta de una ruta de acceso a un directorio restringido (CWE-22). Tiene muchas variantes y, según las 25 debilidades de software más peligrosas de CWE, seguirá siendo muy común y peligrosa en 2022. Exploraremos tres tipos diferentes de directory traversal y relacionaremos las vulnerabilidades con ejemplos de código vulnerable, soluciones y posibles consecuencias.

Lectura arbitraria de archivos

Los servidores web suelen implementar funciones para servir recursos estáticos como archivos HTML, CSS y JS. Por lo general, los recursos se almacenan en carpetas específicas (por ejemplo, /static, /www o /assets) del sistema de archivos. Se produce una vulnerabilidad de lectura arbitraria de archivos cuando la aplicación web no sanitiza correctamente la ruta al archivo estático, lo que permite al usuario usar segmentos de ruta “../” para salir de la carpeta prevista y, finalmente, leer cualquier archivo del disco.

Pasemos directamente al ejemplo de esta vulnerabilidad que descubrimos hace poco en el framework web Crow. Crow es un microframework de C++ para ejecutar servicios web.

Para este ejemplo, crearemos una aplicación sencilla de “hola mundo” que constará de un solo archivo main.cpp:

#define CROW_MAIN
#include <crow_all.h>

int main()
{
    crow::SimpleApp app;

    CROW_ROUTE(app, "/")
    ([]() {
        return "Hello world!";
    });

    app.port(8000).run();
    return 0;
}

Para compilar el ejemplo, usamos Ubuntu: g++ -pthread -o server main.cpp. A partir de aquí, se puede ejecutar el servidor con ./server en la terminal.

Según la documentación, de forma predeterminada, Crow sirve archivos estáticos desde la carpeta /static, ubicada en el mismo lugar que el archivo ejecutable server. Podemos probarlo creando la carpeta /static y un archivo de texto en ella: mkdir static && echo “test” > static/test.txt

Ahora, desde otra ventana de terminal, podemos ejecutar curl para comprobar si funciona:

curl http://localhost:8000/ nos devuelve Hello world! como respuesta.

curl http://localhost:8000/static/test.txt muestra test, que es el contenido del archivo static/test.txt.

Para aprovechar la vulnerabilidad, podemos ejecutar curl --path-as-is "http://localhost:8000/static/../../etc/passwd". En la respuesta, veremos el contenido del archivo /etc/passwd. El truco está en la opción --path-as-is, que desactiva la normalización de rutas en las URL. Esto significa que el servidor recibirá exactamente la URL que proporcionamos, incluidos los segmentos “../”.

Implicaciones de seguridad

La filtración de archivos arbitrarios del servidor de producción suele ser un problema crítico: las claves SSH, diversas credenciales y el código fuente del servidor pueden permitir que un actor malicioso intensifique el ataque y tome el control del servidor o de datos confidenciales de los usuarios.

Lo mismo ocurre con los dispositivos integrados, donde se usan servidores C++ con mayor frecuencia. Esta vulnerabilidad de directory traversal aparece con frecuencia en routers Wi-Fi: NETGEAR, Belkin, TP-Link, entre otros. En este caso, una posible consecuencia sería el robo de credenciales del panel de administración y la obtención del control total de la red local.

En algunos casos, el problema de directory traversal puede ser peligroso incluso si el servidor vulnerable se ejecuta localmente en tu equipo. Los servidores web locales suelen usarse en aplicaciones híbridas de escritorio y web como Electron. Para leer más sobre este tipo de vectores de ataque, consulta nuestra investigación sobre plugins vulnerables de VS Code.

Mitigación

Crow v0.3+4 corrigió esta vulnerabilidad de directory traversal. El problema se introdujo en el archivo app.h, donde el código simplemente concatena una parte de la URL con la ruta de la carpeta estática y llama al método set_static_file_info para servir el archivo:

res.set_static_file_info(CROW_STATIC_DIRECTORY + file_path_partial);

En este caso, la solución consistió en agregar una llamada a utility::sanitize_filename(path) al inicio del método set_static_file_info. sanitize_filename reemplaza todas las apariciones de “..” por “_”, lo que corrige la vulnerabilidad. La solución no es perfecta porque el servidor no podrá servir archivos cuyo nombre contenga “..”, pero probablemente esto sea poco común y no cause problemas importantes.

Escritura arbitraria de archivos

Las vulnerabilidades de escritura arbitraria de archivos son muy comunes en bases de código donde la aplicación permite subir archivos. La causa principal es muy similar a la del caso anterior, aunque un poco más difícil de detectar. Imagina un formulario HTML sencillo para subir archivos:

<form method=”post”>
<input type="file" name="avatar">
<input type="submit" value="submit">
</form>

Probemos seleccionar un archivo de texto llamado “test.txt” con el contenido “test”. El navegador generará una solicitud HTTP similar a la siguiente:

POST /upload HTTP/1.1
Host: localhost:8000
Content-Length: 150
Content-Type: multipart/form-data; boundary=----xxx
Connection: close

------xxx
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

test

------xxx--

Si observas más detenidamente el cuerpo de la solicitud, verás que contiene más información que solo el contenido del archivo. En concreto, incluye un campo de nombre de archivo que puede contener segmentos “../” y que el servidor debe manejar correctamente.

Para mostrar el problema, usaremos una vulnerabilidad de escritura arbitraria descubierta recientemente en Mongoose, un framework de servidor web integrado muy popular: Mongoose.

El ejemplo de carga de archivos del repositorio de Mongoose permite que los usuarios suban archivos en fragmentos, lo cual resulta muy práctico si el dispositivo donde se ejecuta el servidor tiene poca memoria RAM. Podemos compilar y ejecutar el ejemplo con solo clonar el repositorio y ejecutar el comando make en la carpeta del ejemplo. El servidor empieza a escuchar de inmediato en el puerto 8000 y establece /tmp como carpeta de destino para los archivos subidos (tal como se indica en la línea 13 del archivo main.c).

En este punto, podemos usar curl para subir archivos. El comando curl -X POST --data-binary "test" http://localhost:8000/upload?offset=0&name=test.txt creará el archivo /tmp/test.txt en el servidor con el texto “test”.

Para aprovechar la vulnerabilidad, podemos agregar segmentos de ruta “../” justo antes del parámetro de consulta name: curl -X POST --data-binary "pwned" http://localhost:8000/upload?offset=0&name=../malicious-file. El exploit creará el archivo malicious-file en la raíz del sistema de archivos.

Implicaciones de seguridad

Aprovechar vulnerabilidades de escritura arbitraria de archivos no es tan sencillo como aprovechar vulnerabilidades de lectura arbitraria, pero en muchos casos aún puede dar lugar a la ejecución remota de código (RCE). A menudo, la RCE es posible porque un actor malicioso puede sobrescribir archivos importantes del sistema, como /etc/init.d/, o incluso los propios archivos ejecutables del servidor.

Mitigación

Al igual que en el ejemplo anterior, Mongoose corrigió la vulnerabilidad eliminando “..” del nombre de archivo proporcionado por el usuario. La solución se publicó como parte de la versión 7.6.

Como recomendación general, sugerimos evitar los nombres de archivo proporcionados por el usuario y usar cadenas aleatorias, marcas de tiempo o hashes MD5 en lugar de los nombres originales. Esto suele ser aplicable a las aplicaciones que permiten subir archivos y ayuda a evitar muchas otras vulnerabilidades, como XSS.

Zip slip

El tercer tipo de problema de directory traversal que abordaremos hoy es zip slip. Como su nombre lo indica, se trata de vulnerabilidades relacionadas con la lógica de extracción de archivos comprimidos. Este tipo es muy similar a la escritura arbitraria de archivos, pero tiene una superficie de ataque más limitada porque la lógica de extracción de archivos comprimidos no es tan común.

Para demostrar la vulnerabilidad zip slip, usaremos la versión vulnerable 6.1.4 de JUCE, un framework de código abierto y multiplataforma para aplicaciones C++.

#include <juce_core/juce_core.h>

int main()
{
    juce::File file("archive.zip");
    juce::ZipFile zipFile(file);
    zipFile.uncompressTo(juce::File("data"));
    return 0;
}

Esta es una aplicación muy sencilla que usa la API de JUCE para extraer archive.zip en la carpeta data.

Para ver cómo funciona, podemos crear un archivo comprimido de ejemplo con un archivo test.txt:

echo test > test.txt && zip archive.zip test.zip
  adding: test.zip (stored 0%)

Si compilamos y ejecutamos nuestra aplicación de ejemplo, se creará la carpeta data y el archivo test.txt quedará dentro. Pero ¿qué pasa si creamos un archivo comprimido con el archivo un nivel más arriba en la jerarquía de archivos?

echo bad > ../bad.txt && zip archive.zip ../bad.txt

En este caso, si ejecutamos nuestro programa, el archivo bad.txt quedará fuera de la carpeta de destino data. Como puedes ver, los riesgos de seguridad de zp slip son exactamente los mismos que en el caso de la escritura arbitraria de archivos.

Existen otras variantes de zip slip (como la extracción de enlaces simbólicos). Te recomendamos leer nuestro artículo de investigación para comprender mejor este tema.

Mitigación

La vulnerabilidad que mostramos se corrigió en la versión 6.1.5 del framework JUCE, junto con otra variante: zip slip mediante enlaces simbólicos. JUCE corrigió la vulnerabilidad verificando que la ruta completa resuelta estuviera dentro del directorio de destino.

Recomendaciones generales para corregir estas vulnerabilidades

Al investigar esta clase de vulnerabilidad, el equipo de Snyk Security Research revisó muchísimos ejemplos de código que implementaban distintas lógicas relacionadas con rutas del sistema de archivos. Creemos que encontramos una regla general que puedes seguir para protegerte de las vulnerabilidades de directory traversal.

Si puedes evitar usar rutas de archivo proporcionadas por el usuario, ¡hazlo!

Esta regla se aplica a muchos casos de lógica de carga de archivos y extracción de archivos comprimidos.

Pero si tienes que usar rutas de archivo proporcionadas por el usuario, te recomendamos seguir esta lógica (implementada con std):

// #include <iostream>
// #include <string>
// #include <filesystem>
// #include <algorithm>

    // This is the path to your static files folder.
    std::string base_path("/tmp/path_to_the_content_root_directory/");
    // User input – possibly contains "../" path segments.
    std::string user_input("user_provided_path");

    // The "canonical" method resolves ".", ".." and symlinks.
    std::filesystem::path base_resolved_path(
        std::filesystem::canonical(base_path));
    // The "weakly_canonical" method does the same as "canonical" but not requires
    // the file to exist.
    std::filesystem::path requested_file_path(
        std::filesystem::weakly_canonical(base_resolved_path / user_input));

    // Using "equal" we can check if "requested_file_path" starts
    // with base_resolved_path. Because we previously canonicalized both paths
    // they can't contain any ".." segments, so this check is sufficient.
    if (std::equal(
            base_resolved_path.begin(),
            base_resolved_path.end(),
            requested_file_path.begin())) {
        std::cout << "It is safe to work with the file: " << requested_file_path << "\n";
    } else {
        std::cout << "Not safe! We have to throw an exception here.\n";
    }

El código anterior requiere al menos C++17, pero también se puede implementar con boost. Si tu configuración no dispone de std ni de boost, también es suficiente con reemplazar “..” por cadenas vacías y luego convertir varias barras “/” consecutivas en una sola.

Además, si tu aplicación debe ser multiplataforma y también se ejecutará en Windows, considera normalizar los caracteres separadores de directorios. En la mayoría de los casos, Windows acepta tanto la barra (“/”) como la barra invertida (“\”) como separador de directorios. Esto significa que la secuencia “..\” equivale a “../” y también se puede usar para llevar a cabo un ataque de directory traversal. La forma más sencilla de garantizar que tu código sea seguro en Windows es reemplazar “/” por “\” antes de procesar las rutas.

Conclusión y hallazgos clave

Cuando los profesionales de seguridad de la información piensan en vulnerabilidades de C y C++, suelen centrarse en problemas de bajo nivel, como distintos tipos de corrupción de memoria. Aunque estos problemas tienen un gran impacto en el ecosistema, también debemos tener en cuenta el contexto de las aplicaciones y prevenir las vulnerabilidades de alto nivel, además de las de bajo nivel. Al iniciar la investigación, suponíamos que los desarrolladores web de C/C++ no prestaban suficiente atención a los problemas comunes de la web. Y, efectivamente, descubrimos muchas vulnerabilidades de directory traversal:

  • CVE-2022-25299: escritura arbitraria de archivos en Mongoose, un servidor web integrado.

  • CVE-2022-25297: escritura arbitraria de archivos en Drogon, un framework de aplicaciones HTTP basado en C++14/17.

  • CVE-2021-23520, CVE-2021-23521: zip slip en JUCE, un framework de código abierto y multiplataforma para aplicaciones C++.

  • CVE-2021-23514: recorrido de rutas en Crow, un microframework de C++.

  • CVE-2022-25298: recorrido de rutas en Webcc, un cliente y servidor HTTP ligero de C++.

Ten en cuenta que esta lista sigue creciendo y la actualizaremos a medida que los colaboradores publiquen sus correcciones.

¡Mantente protegido!

Empieza con los retos de Capture the Flag

Aprende a resolver retos de Capture the Flag viendo nuestro taller virtual de nivel básico a pedido.