De Blind XXE a lectura de archivos con permisos de root

Por Berenice F (@dark1t)

En este write-up se explica la solución del reto BNV de la categoría Web que se resolvió durante el Google CTF 2019.

La URL y descripción del reto era la siguiente:

There is not much to see in this enterprise-ready™ web application.
https://bnv.web.ctfcompetition.com/

Al navegar a la página del reto se encontró un simple input select y un submit para seleccionar una de tres ciudades (Zurich, Bangalore, Paris), el background en negro y varias referencias a la palabra "blind" (ciego). Inclusive, el banner de la página mostraba un mensaje de bienvenida en sistema Braille.


Se interceptó el request al seleccionar Zurich como ciudad y nos encontramos con una petición POST de tipo JSON con un parámetro message y números como valor de ese parámetro. Al enviar el request se recibía información de la ciudad en un parámetro llamado ValueSearch.


La página se veía bastante simple y el request no decía mucho, entonces se accedió al código fuente en busca de alguna pista u otra información importante. En el código fuente se pudo observar que se estaba llamando un script post.js


Análisis del script post.js

Se accedió al contenido del script con la URL https://bnv.web.ctfcompetition.com/static/post.js y después de analizarlo se encontró lo siguiente:
  • La palabra del input select se codificaba a braille (números) y se le agregaba un cero (0) al final de cada letra, de tal forma que a = 10, b = 120. Lo anterior se determinó con ayuda del alfabeto Braille y concluyendo que a cada dígito del número correspondía una posición.

z = 13560
u = 1360
r = 12350
i = 240
c = 140
h = 1250
zurich = 1360123502401401250
  • La aplicación envía el número codificado del parámetro message usando un POST request a la dirección /api/search en formato JSON por medio de la función XMLHttpRequest(). Ésta función permite utilizar diferentes tipos de contenido (Content-Type) como JSON o XML.
  • El servidor del reto devolvía el resultado de la búsqueda en formato JSON con el parámetro ValueSearch.
El contenido del script post.js es el siguiente:

function AjaxFormPost() {
  var datasend;
  var message = document.getElementById('message').value;
  message = message.toLowerCase();

  var blindvalues = [
    '10',    '120',   '140',    '1450',   '150',   '1240',  '12450',
    '1250',  '240',   '2450',   '130',    '1230',  '1340',  '13450',
    '1350',  '12340', '123450', '12350',  '2340',  '23450', '1360',
    '12360', '24560', '13460',  '134560', '13560',
  ];

  var blindmap = new Map();
  var i;
  var message_new = '';

  for (i = 0; i < blindvalues.length; i++) {
    blindmap[i + 97] = blindvalues[i];
  }

  for (i = 0; i < message.length; i++) {
    message_new += blindmap[(message[i].charCodeAt(0))];
  }

  datasend = JSON.stringify({
    'message': message_new,
  });
  var url = '/api/search';
  xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.setRequestHeader('Content-type', 'application/json');

  xhr.onreadystatechange =
      function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        console.log(xhr.getResponseHeader('Content-Type'));
        if (xhr.getResponseHeader('Content-Type') == "application/json; charset=utf-8") {
            try {
                var json = JSON.parse(xhr.responseText);
                document.getElementById('database-data').value = json['ValueSearch'];
            }
            catch(e) {;
                document.getElementById('database-data').value = e.message;
            }
        }
        else {
            document.getElementById('database-data').value = xhr.responseText;
        }
    }
}
      xhr.send(datasend);
}

Después de analizar el script, se intentaron varios tipos de inyección y ataques conocidos en JSON, pero no se encontró algo prometedor. También se intentó enviar payloads codificados en Braille pero no se obtuvo algún resultado satisfactorio.
Poco después a @nogagmx se le ocurrió mandar un request con contenido tipo XML en lugar de JSON. Esto nos respondió algo favorable, ya que al parecer la aplicación procesaba adecuadamente éste lenguaje.

Petición XML

Formamos una petición en formato XML con el contenido del parámetro message, que en este caso fue el valor codificado de Zurich  y se estableció la cabecera Content-Type igual a application/xml. El servidor respondió el mismo mensaje de antes correspondiente a Zurich, pero ahora sin formato JSON.


Con lo anterior vino a la mente el ataque XXE (XML External Entity attack), con el cual es posible que un  atacante interfiera en la forma en que la aplicación web maneja datos XML y entre otras cosas, permite que éste pueda leer archivos de la víctima o que pueda interactuar con el backend.

La descripción del reto no proporcionaba información acerca de la ubicación de la bandera o cómo obtenerla, por lo que como primer paso se decidió intentar leer archivos usando el ataque XXE mencionado anteriormente.

Para realizar un ataque XXE que muestre un archivo de la máquina víctima es necesario enviar una petición con data en formato XML, el cual debe tener un elemento tipo DOCTYPE que defina una entidad externa (external entity) y que ésta contenga el path del archivo. Para más información acerca del formato XML se puede consultar este artículo.

Intentando leer archivos y directorios de la víctima

Un archivo presente comúnmente en sistemas Unix es /etc/passwd. Usualmente, este archivo se utiliza para validar si existe una vulnerabilidad del tipo lectura arbitraria de archivos de sistema. Tomando en consideración todo lo mencionado anteriormente, se envió la siguiente petición al servidor y nos respondió "No result found" (este error se obtenía cuando se enviaba algún valor inexistente en el parámetro message, es decir un valor diferente a las 3 ciudades listadas en el input select)


Como siguiente paso intentamos hacer la petición a otros archivos conocidos en sistemas Unix (/etc/hosts, /etc/resolv.conf) y obtuvimos el mismo resultado.
Para poder validar si el resultado de la petición nos estaba diciendo algo, se hizo una petición a un archivo inexistente, simplemente llamado "mayas". Para nuestra sorpresa nos encontramos esta vez con un error diferente: "Failure to process entity xxe".


Con el resultado anterior se determinó que estábamos frente a un caso de ataque blind (ciego) XXE, siendo blind porque la respuesta que recibimos del servidor no es el contenido del archivo, sino mensajes de error, que se interpretan como una validación dependiendo si existe el archivo o no. Las condiciones TRUE/FALSE que se encontraron fueron las siguientes:

😀TRUE (Archivo existe en la máquina de la víctima): No result found
😟FALSE (Archivo no existe en la máquina de la víctima): Failure to process entity xxe

Hasta este punto no sabíamos si existía algún archivo cuyo contenido fuera la bandera y que pudiéramos leer, así que a modo de prueba intentamos hacer una petición a un archivo llamado "flag". Dió como resultado una condición verdadera y nos dió la pista que estábamos en el camino correcto para obtener la bandera.
Por último, se intentó leer el contenido de directorios siguiendo un proceso similar. En la petición se establecieron los directorios /root (true) y /test (false) y se obtuvieron los siguientes errores que validaban la condición:

😀TRUE (Directorio existe en la máquina de la víctima): 500 Internal Server Error
😟FALSE (Directorio no existe en la máquina de la víctima): Failure to process entity xxe


OOB XXE (Out of band XXE)

El siguiente paso que se tomó fue verificar si se podía explotar la aplicación con el ataque XXE OOB. Este tipo de ataque consiste en redireccionar el contenido del archivo de la víctima a un servidor del atacante (en ésta página se pueden encontrar ejemplos de este ataque). 
Durante el CTF se intentó aplicar este tipo de ataque utilizando protocolos como http://, https://, ssh, gopher://, ftp://, pero todos dieron el mismo resultado no satisfactorio y no se recibió una petición en nuestro servidor (Ngrok), probablemente debido a la existencia de un firewall o a alguna restricción en la configuración del servidor.


XXE Error-Based (o leyendo archivos de la víctima con ayuda de un archivo local DTD)

En este momento nos enfrentábamos a dos limitantes: la aplicación no mostraba el contenido de los archivos al hacer la petición XML y existía una restricción o firewall que impedía redireccionar el contenido del archivo de la víctima a nuestro servidor atacante.
Investigamos un poco y descubrimos que existe una técnica de ataque XXE que abusa de archivos locales DTD, de tal manera que al intentar usar dicho archivo local, el servidor nos respondería con un error y el contenido del archivo local que deseamos leer.
Esencialmente, este ataque invoca un archivo DTD que existe localmente en el sistema de archivos de la víctima y se utiliza para re definir una entidad (entity) existente, lo cual desencadena un error que contiene la información sensible de la máquina de la víctima.
Ahora nos enfrentábamos al siguiente reto: saber la ubicación de un archivo DTD existente en la máquina de la víctima. Afortunadamente, Portswigger tiene una muy buena guía y laboratorio del ataque y menciona la ubicación de un archivo DTD que se encuentra comúnmente en sistemas Linux que utilizan Gnome como entorno de escritorio.


Después de descubrir lo anterior, se validó si el directorio del archivo mencionado (docbookx.dtd) existía en la máquina de la víctima. Para esto, se usó el ataque blind XXE para intentar leer el contenido de un directorio y se mandó una petición con el path del directorio en donde se localiza el archivo DTD (/usr/share/yelp/dtd). 
La petición dió como resultado error 500, que corresponde a una condición verdadera (TRUE) según lo analizado previamente al intentar leer directorios de la víctima.

Por último, una vez que se validó que el directorio del archivo .dtd existía en la máquina de la víctima, se realizó una petición al archivo flag con el siguiente payload que hacía uso del archivo .dtd:

<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
    <!ENTITY % ISOamso '
        <!ENTITY &#x25; file SYSTEM "file://flag">
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;error;
    '>
    %local_dtd;
]> 

Una vez que se mandó una petición con el payload anterior se obtuvo la bandera:


Flag: CTF{0x1033_75008_1004x0}

Extra Mile

Para finalizar, se analizó qué tipo de archivos podíamos leer de la máquina de la víctima. Se observó que podiamos leer archivos como /etc/passwd o /etc/shadow. Con éste último archivo se concluyó que se podían leer archivos con permisos de root.


Go Mayas!

Comentarios