Filtrando mis contactos del celular con AWK

Hace unas semanas cambié mi celular con Android. Una de las cosas que tuve que hacer fue importar los contactos del celu viejo al nuevo. Como no uso soluciones de almacenamiento en la nube por cuestiones de privacidad, la importación la hice de forma manual, usando ficheros vCard (con extensión .vcf).

En su momento, cuando todavía usaba Android con los servicios de Google, a la aplicación de Gmail se le ocurrió crear un contacto para cada usuario con quien haya intercambiado algún correo electrónico. Esto me creó un montón de contactos que no aportan nada en mi celular. Contactos a quienes les escribí por única vez, sin intención de hacerlo nuevamente. De quienes solamente conozco su dirección de correo electrónico y no su celular.

Como estaba cambiando el celu, me pareció que sería una buena ocasión para borrar todos estos contactos innecesarios. Para esto necesitaría:

  • Exportar todos mis contactos del celular viejo a un fichero .vcf
  • Encontrar o armar una herramienta que borre todos aquellos contactos que no tengan número de teléfono
  • Grabar la salida en un nuevo fichero .vcf, listo para ser importado en el celular nuevo

Para escribir una herramienta que filtre contactos tendría que ser capaz de parsear un fichero .vcf. Normalmente hubiese usado Python para resolver el problema. Podría haber utilizado una librería externa y confiar en que no tenga bugs ni vulnerabilidades. O podría haber creado mi propia librería para manejo de .vcf/vCard, propiamente testeada y documentada. Sin embargo, ambas opciones parecían demasiado complicadas para un programa que pretendo correr una única vez. Tiene que haber una solución más simple.

Un fichero vcf tiene el siguiente formato:

BEGIN:VCARD
VERSION:2.1
N:Nombre;Apellido;;;
FN:Nombre Visible
TEL;CELL:123-456-789
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:Otro;Contacto;;;
FN:Otro contacto de Gmail
EMAIL;PREF:usuario@gmail.com
END:VCARD

Como se ve, los detalles de cada contacto se encuentran entre las líneas BEGIN:VCARD y END:VCARD. El formato en sí no parece nada complicado, ya que es texto plano delimitado por líneas.

Teniendo en cuenta de que quería parsear un fichero en un formato simple y hacerlo una única vez, me decidí por usar AWK en vez del tradicional Python. El lenguaje AWK es bastante chico y por esto se puede aprender en pocas horas. Con leer su página en Wikipedia ya se puede entender bastante su funcionamiento.

Después de repasar un poco, me armé un programa de awk corto pero efectivo:

# lineas va a guardar las líneas del contacto a procesar en un array.
# n es la longitud del array, que se incrementa en cada iteración.
{
    lineas[n++] = $0; # Esto sería similar a un append en Python
}

/^TEL;/ {
    # el contacto siendo procesado tiene un teléfono, así que quiero que se muestre
    tiene_telefono = 1
}

/^END:VCARD/ { # llegué al final del contacto, tengo que decidir si mostrarlo o no

    # si tenía un teléfono, mostrar todas las líneas que tengo guardadas
    if (tiene_telefono)
        for (i=0; i<n; i++)
            print lineas[i]

    # en la próxima iteración voy a arrancar con nuevo contacto, así que
    # reinicio el estado del programa.
    tiene_telefono = 0
    n = 0 # Esto simula varias el array
}

Después ejecuté el programa corriendo awk -f miprograma.awk <contactos-sin-filtrar.vcf >contactos-filtrados.vcf. Eso me generó un nuevo .vcf con los contactos ya filtrados, listo para importar en mi celular.

Con solamente 13 líneas de código (sin contar comentarios ni líneas vacías), logré armar un programa que solucionó mi problema a la perfección. No me compliqué instalando librerías externas, creando jerarquías de clases inentendibles, ni haciendo parsers complejos de algún formato.

Así parece que con un lenguaje con más de 40 años de antigüedad me sentí más cómodo que con Python, mi lenguaje de preferencia. Esto se debe a que AWK es un lenuaje especializado en manejo de ficheros de texto y programas de un solo uso. Quizás el código no sea tan mantenible, pero no me importa si se que no lo voy a volver a correr. Necesitaba una solución rapida, y AWK cumplió a la perfección.

Espero con este post haber explicado la esencia del lenguaje AWK. Me parece que es una herramienta fundamental para cualquier persona que se dedique a programar o administrar sistemas UNIX-style. El lenguaje se puede aprender en pocas horas, y es capaz de mejorar muchísimo nuestra productividad.

Algunos recursos que me sirvieron para aprender AWK (están en inglés):

También existe un libro sobre el lenguaje escrito por sus autores. No lo puedo recomendar porque todavía lo leí, pero en caso de que los recursos anteriores te dejen con ganas de más, este libro seguro es el siguiente paso.

Saludos!

Ekoparty CTF: Stegano Writeup

Hace unos días fue la Ekoparty Online 2020. Esta vez la conferencia fue online, así que opté por participar activamente en el CTF en vez de asistir a las charlas. Muchos retos me resultaron bastante desafiantes, entre ellos Stegano, uno de los últimos que pudimos resolver junto a EzequielTBH.

Para este reto se nos entregaba una imagen en formato BMP, así que nos imaginamos que la flag iba a estar escondida ahí. Subiendo la imagen al buscador Google Images y agregando la palabra clave steganography, nos encontramos con un blogpost hablando de la esteganografía usada por Loki-Bot. Sabiendo que la temática del CTF era el malware, nos imaginamos que el problema venía por ahí.

Leyendo el blogpost vimos que en una parte se describía, mostrando código C, el proceso por el cual se descifra un mensaje aplicándole un XOR con la clave @y_%_M_ew@ y con la variable de un byte dwKeysize, que representa la longitud de esta clave (en este caso, 10 o 0xA):

Descifrado de mensajes usando XOR

También se mencionaba un proceso por el cual se obtiene el mensaje escondido dentro del fichero BMP:

Proceso de obtención del mensaje oculto en el BMP

Si bien el proceso de descifrado usando XOR se entendió bastante bien, este código para obtener el mensaje en el BMP era algo confuso y tenía bastantes constantes que no entendimos de dónde se sacaron. Hacer un programa que obtenga el mensaje de forma perfecta nos hubiese llevado bastante tiempo, y faltaba poco para que el CTF termine. Sin embargo, la perfección no es necesaria para resolver un reto. Lo único que necesitábamos era obtener la flag, que tiene el formato EKO{...}. No nos importa que la información descifrada conserve su integridad, sino que nos podemos permitir que tenga algunos bytes que son basura, siempre y cuando nos muestre la flag. Teniendo en cuenta esto, usamos una versión simplificada del algoritmo de descifrado. No es para nada perfecta, pero cumplió su propósito.

Si volvemos al código en C que lee el fichero BMP podemos notar algunas cosas:

  • Se calcula desde qué byte empezar a descifrar teniendo en cuenta el header del fichero BMP, el ancho y alto de la imagen
  • Mucha de la lógica en el código consiste en detectar cuándo se llegó al final de una fila para así pasar a la fila siguiente, arrancando desde la primer columna. Los bloques if dentro del código se encargan de hacer esto.
  • En cada iteración del for, se leen dos bytes del fichero (esto se puede ver en el código resaltado). El & 0x0F se queda solamente con los últimos 4 bits de cada byte, ignorando los bits más significativos.
  • Se juntan los 4 últimos bits del segundo byte leído con los 4 últimos del primero, para así escribir un byte por cada dos leídos

Teniendo en cuenta que el tamaño de la flag es de unos pocos bytes y que el ancho del BMP es de 801 píxeles, es poco probable que la flag esté distribuida en dos filas distintas, así que todo el código relacionado a los límites de la imagen se puede ignorar. Además, como no nos importa descifrar información corrupta, siempre y cuando se encuentre la flag, tampoco es necesario calcular desde dónde empezar a descifrar: para simplificar el código, podemos descifrar todo desde el inicio hasta el final de la imagen.

Sabiendo esto, es posible tener una solución más simple al código en C descripto en el blogpost. Solamente habría que ir leyendo el fichero BMP de a dos bytes, juntarlos en un único byte usando operaciones a nivel de bits, y aplicarle un XOR con la clave @y_%_M_ew@. Esto sería bastante sencillo, aunque es necesario tener en cuenta algunas cosas:

  • Como se lee de a dos bytes, no es lo mismo arrancar en un byte par que en uno impar. Una de las dos formas va a producir información incorrecta.
  • El cifrado XOR depende de la posición en la que se arranca. Si arrancamos a descifrar desde una posición incorrecta, la clave puede quedar "desfasada" y descifrar cualquier cosa

Como solución a estos problemas, usamos un método sucio pero eficiente: probemos todas las combinaciones hasta dar con el resultado. Podemos leer primero desde un byte par, después desde uno impar. O podemos probar con todas las posibles rotaciones de la clave (como es de 10 bytes, hay 10 posibles rotaciones).

Finalmente, armamos el siguiente script de Python que lee la imagen y prueba descifrarla usando las combinaciones descriptas anteriormente:

import sys

with open('stegano.bmp', 'rb') as fp:
    bmp_contents = fp.read()

out = open('stegano-output', 'wb')

def try_decode(key, contents):
    encoded = []
    for i in range(0, len(contents)-1, 2):
        first_byte, second_byte = contents[i], contents[i+1]
        encoded.append(((second_byte & 0x0f) << 4) | (first_byte & 0x0f))
    decoded = [(c ^ 0xA ^ key[i%len(key)]) for (i, c) in enumerate(encoded)]
    out.write(bytes(decoded))


ORIGINAL_KEY = b'@y_%_M_ew@'

def possible_keys():
    # rotate the original key
    for i in range(len(ORIGINAL_KEY)):
        yield ORIGINAL_KEY[i:] + ORIGINAL_KEY[:i]

for key in possible_keys():
    try_decode(key, bmp_contents)
    try_decode(key, bmp_contents[1:])  # arrancar desde un byte impar

Una vez que lo ejecutamos y abrimos el fichero stegano-output con un editor hexadecimal, encontramos la flag que resultó siendo EKO{n0m0r3m4lw4r3_and_m04r_st3g4n00000000}:

Flag en el fichero stegano-output

Así pudimos resolver este reto de esteganografía. Si bien este tipo de retos no suelen ser mis preferidos, en este caso lo disfruté bastante. No es el clásico ejemplo de una imagen pasada por el steghide o herramientas similares, sino que requería entender el código que usaba un malware para ocultar la información.

Para concluir el post, quiero destacar la importancia de reconocer que el objetivo de un reto es obtener la flag. Teniendo en cuenta esto, es posible obviar ciertos detalles que no aportan mucho y son bastante tediosos, como fue en este caso la lógica de decidir qué bytes usar para descifrar. Si no hubiese tenido esto en cuenta, quizás no hubiese podido resolver el reto a tiempo.

Espero con este post haber descripto no solo la solución al reto, sino también el proceso que seguimos para solucionarlo. Pronto publicaré las soluciones a algún otro reto que me haya gustado.

Saludos!

Invitación a nerdear.la

El 17, 18 y 19 de octubre de este año se llevará a cabo Nerdear.la: una conferencia sobre devops, desarrollo y temática nerd en general. Se trata de la sexta edición del evento, y es la primera vez que se hace en Ciudad Cultural Konex. La entrada es 100% gratis, al igual que en años anteriores.

Nerdearla 2018

Este año, la conferencia contará con un excelente nivel de charlas que nada tiene que envidiarle a otros eventos, locales o internacionales. El jueves a las 16:35 yo voy a estar dando una charla sobre entornos reproducibles y cómo utilizar Nix como alternativa a Dockerfiles. Acto seguido, GiBA va a hablar sobre una herramienta de logging bastante prometedora que hicieron en Facebook.

Otras charlas que me interesaron:

  • Making Illegal States Unrepresentable (in JavaScript): por el título, supongo que va a hablar un poco de Elm, uno de mis lenguajes de programación preferidos, y mostrar como se pueden adaptar ciertas cosas del lenguaje a Javascript.
  • Diagnosing bad TDD habits with Dr.TDD: una charla sobre TDD y Smalltalk, que encima tiene un video en su descripción! Definitivamente no me la pienso perder.
  • Donald Knuth, TeX y la curva del dragón: hablará de TeX, el predecesor de LaTeX. Si bien no es un sistema que me guste mucho, tiene una historia muy interesante, en especial por tratarse de uno de los proyectos más importantes de Donald Knuth.
  • Standup Matemático: por si creían que para llamarse "nerdear.la" le faltaban cosas nerd.
  • 1969 - 2019: 50 years of UNIX and the landing on the Moon: charla de cierre dada por el conocido Jon «maddog» Hall. No pude asistir a la charla que dio la última vez que estuvo en Argentina, así que esta es mi oportunidad de hacerlo!

También estarán dando un taller de introducción a Bash dictado por las chicas de LinuxChix Argentina que vengo recomendando hace unos días a gente con poca experiencia con el uso de la terminal en GNU/Linux. Es de cupo limitado así que hay que inscribirse primero.

Y para quienes tengan que trabajar durante los días de la conferencia, también contará con un espacio de coworking que, al menos hasta el año pasado, siempre contó con una buena conexión a internet.

Para más información sobre el evento, puden visitar su sitio https://nerdear.la/.

Saludos!

Apéndice

No encontré en la página la descripción completa de mi charla, así que la dejo por acá para quien esté interesado/a:

“It works on my machine!” Seguramente todo desarrollaror/a haya usado esta frase en algún momento de su carrera para justificar la presencia de un bug. Decir esto implica, más allá del bug en cuestión, que hay una diferencia no muy evidente (pero sí perjudicial) entre los entornos de desarrollo y el productivo.

En los últimos años se trató de minimizar estas diferencias por medio de entornos reproducibles. Las herramientas de cloud computing, infrastructure as code y principalmente el uso de containers facilitaron esto. Sin embargo, estas herramientas no siempre garantizan que lo que hagamos sea reproducible. Además, en varios casos su complejidad de uso hace que se usen solamente en entornos de staging y no en las máquinas de los desarrolladores, lo que nos vuelve a llevar al “it works on my machine”.

Voy a hablar de Nix, un lenguaje de programación, package manager y build tool que tiene a la reproducibilidad como idea principal. Nix permite construir un entorno reproducible sin tener el overhead que traen el uso de containers o VMs (aunque también se lleva muy bien con estos). De esta forma, resulta muy conveniente tanto al momento de desarrollar como al servir el software en producción.

Bypasseando una sandbox de JS restrictiva

Participando en un programa de bug bounties encontré un sitio con una funcionalidad bastante interesante que permitía filtrar un conjunto de datos según una expresión controlada por el usuario. Es decir, podía ponerle algo como book.price > 100 para que solamente me muestre los libros que salgan más que $100. Usando true y false como filtros me mostraba todos y ningún resultado, respectivamente. Por lo que podía saber si el resultado de la expresión que le pasé se evaluaba a verdadero o falso.

Como la funcionalidad me llamó la atención seguí probando con expresiones más complejas como (1+1).toString()==="2" (verdadero) y (1+1).toString()==="5" (falso). Esto es claramente código JavaScript, así que supuse que la expresión se le pasaba a una función similar a eval en un server corriendo NodeJS, por lo que parecía que estaba cerca de encontrar un RCE (remote code execution). Sin embargo, cuando probaba poner expresiones un poco más complejas el sistema me tiraba un error diciendo que la expresión era inválida. Asumí que lo que le mandaba pasaba por algún sistema de sandbox para JavaScript.

Los sistemas de sandbox que se usan para ejecutar código ajeno en un entorno restringido suelen ser muy difíciles de implementar, y generalmente existen maneras de bypassear estas protecciones para así ejecutar nuestro código con privilegios normales. Esto es especialmente cierto si se trata de limitar el uso de lenguajes complejos, llenos de funcionalidades ocultas como es el caso de JavaScript. Como el problema ya había logrado tomar mi atención decidí dedicarle un tiempo importante a tratar de romper esta sandbox para aprender un poco del funcionamiento interno de JavaScript, y de paso ganar algunos dólares en caso de encontrar un RCE.

Lo primero que hice fue identificar qué librería usaban para implementar la sandbox, dado que el ecosistema de NodeJS se caracteriza por tener decenas de librerías que hacen exactamente lo mismo, y en muchos casos todas lo hacen mal. También se podía tratar de una solución implementada por el mismo sitio y no por una librería, pero como es algo muy complejo me pareció muy difícil que se hayan tomado el trabajo de desarrollar algo así, por lo que descarté esta posibilidad inmediatamente.

Finalmente, analizando los mensajes de error de la aplicación llegué a la conclusión de que estaban utilizando static-eval, una librería no muy conocida (aunque escrita por substack, alguien bastante conocido dentro de la comunidad de NodeJS). Si bien no fue hecha con el propósito de ser usada como una sandbox para ejecutar código desconocido (debo reconocer que todavía no entendí para qué fue hecha), su documentación da a entender esto. En el caso del sitio que estaba auditando, sí se usaba esta librería como una sandbox.

Rompiendo static-eval

La idea de static-eval consiste en utilizar la librería esprima para parsear la sintaxis del código JS que le mandemos y generar así un AST (Abstract Syntax Tree) de nuestra expresión. Una vez hecho esto, dado el AST generado y un objeto con las variables que se quiere que estén disponibles, se trata de evaluar la expresión. En caso de encontrar algo un poco fuera de lo común, la función falla y el código no es evaluado. Al principio esto me desmotivó un poco porque me di cuenta de que el sandbox era muy restrictivo con el código que se le pasaba. Ni siquiera me permitía poner un for o un while dentro de la expresión por lo que hacer algo que requiera un algoritmo iterativo se hacía casi imposible. De todas formas, seguí tratando de encontrarle algo.

Sin encontrar ningún bug a simple vista, me puse a ver los commits y pull requests del proyecto en GitHub. En particular, el pull request #18 arregló dos bugs que permitían escaparse de la sandbox, exactamente lo que estaba tratando de encontrar. También encontré un blogpost del autor del pull request que explicaba con más detalle estas vulnerabilidades. Lo primero que hice fue probar estas técnicas en la aplicación que estaba testeando, pero desafortunadamente para mí, la versión de static-eval que utilizaban ya había corregido estos fallos. Sin embargo, saber que alguien más ya había roto la librería me dio más confianza así que seguí buscando otra forma de explotarla.

A continuación, me puse a analizar más profundamente las vulnerabilidades ya encontradas para ver si me podía inspirar en alguna para encontrar algo nuevo.

Análisis de la primera vulnerabilidad

Una de las vulnerabilidades encontradas consistía en hacer uso del function constructor. Esta técnica es utilizada frecuentemente para bypassear sandboxes. Por ejemplo, varias de las formas de bypassear la sandbox de angular.js para lograr un XSS usan distintas variaciones que terminan accediendo y llamando al function constructor. También fue usado para bypassear librerías similares a static-eval, como vm2. La siguiente expresión demuestra la existencia de la vulnerabilidad accediendo a las variables de entorno del sistema (algo que debería estar restringido ya que el código se ejecuta en una sandbox):

"".sub.constructor("console.log(process.env)")()

En este código, "".sub es una forma corta de obtener una función (también hubiese servido algo como (function(){})). Luego se accede a su constructor, que es una función que crea nuevas funciones con el código que se le pase como argumento. Esto vendría a ser como una función eval, solo que en vez de evaluar la expresión inmediatamente, retorna una función que ejecutará el código recién cuando sea llamada. Esto explica los paréntesis que están al final del payload, que hacen que se llame a la función creada.

Resultado de ejecutar el payload anterior

No solo es posible mostrar las variables de entorno, sino que también se pueden ejecutar comandos en el sistema, usando la función execSync del módulo child_process de NodeJS. El siguiente payload va a retornar la salida del comando id del sistema:

"".sub.constructor("console.log(global.process.mainModule.constructor._load(\"child_process\").execSync(\"id\").toString())")()

El código es similar al anterior, excepto por el cuerpo de la función que se crea. En este caso, global.process.mainModule.constructor._load viene a ser lo mismo que la función require de NodeJS que se usa para cargar módulos. Por alguna razón que no entendí, esta función no está disponible bajo el nombre require dentro del function constructor.

Resultado de ejecutar el payload que ejecuta el comando id

El fix para esta vulnerabilidad consistió en bloquear el acceso a propiedades de objetos que sean una función (para esto está el typeof obj == 'function'):

else if (node.type === 'MemberExpression') {
    var obj = walk(node.object);
    // do not allow access to methods on Function 
    if((obj === FAIL) || (typeof obj == 'function')){
        return FAIL;
    }

Esto es algo bastante sencillo, pero sorprendentemente efectivo. El function constructor está disponible, naturalmente, solamente en funciones, así que no es posible acceder a este. Además el typeof de un objeto no se puede modificar, por lo que cualquier cosa que sea una función va a tener su typeof fijo a function. No encontré ninguna forma de bypassear esta protección así que seguí con la siguiente vulnerabilidad que habían encontrado.

Análisis de la segunda vulnerabilidad

Esta vulnerabilidad, a diferencia de la primera, es bastante más sencilla: el problema era que la sandbox permitía que se creen funciones anónimas, pero no chequeaba que el código de estas funciones no tenga nada malicioso, sino que se lo pasaba directamente al function constructor. El siguiente código tiene el mismo efecto que el primer payload que se utilizó en este blogpost:

(function(){console.log(process.env)})()

También se puede cambiar el cuerpo de la función para que use el execSync y muestre la salida de ejecutar un comando. Voy a dejar esto como ejercicio para el/la lector/a.

Un posible fix para esto hubiese sido prohibir la declaración de funciones anónimas dentro de las expresiones. Sin embargo esto hubiese hecho imposible el uso benigno de las funciones anónimas (como por ejemplo, hacer un map a un array). Se tuvo que hacer un fix que permita el uso de funciones anónimas "benignas" pero bloquee las maliciosas. Para esto, static-eval analiza el cuerpo de la función al momento de su definición para detectar que no se hagan cosas malas, como acceder al constructor de una función.

Este fix resultó ser mucho más complejo que el de la primera vulnerabilidad, y además Matt Austin (el autor del fix) hizo entender que no estaba seguro de que esto funcione perfectamente. Así que me incliné por buscar un bypass a este fix.

Encontrando una nueva vulnerabilidad

Una cosa que me hacía bastante ruido es que se decidía si la función era buena o mala solamente al momento de definirla, y no al momento de llamarla. Es decir, que no se tenía en cuenta el valor de los argumentos de la función, ya que esto requeriría que se haga un chequeo al momento de llamar a la función.

Mi idea siempre fue tratar de acceder, de alguna forma, al function constructor para que me deje crear cualquier función libremente, esquivando el fix de la primera vulnerabilidad que me lo impedía ya que no me dejaba acceder a propiedades de una función. Sin embargo, ¿qué pasaría si trato de acceder constructor de un parámetro de la función? como su valor no es conocido al momento de definir la función, quizás esto pueda confundir al sistema y hacer que nos la deje pasar. Para probar mi teoría usé esta expresión:

(function(algo){return algo.constructor})("".sub)

Si esto retornara el constructor de una función ya tendría un bypass efectivo. Lamentablemente no fue el caso, ya que static-eval bloquea la función si se accede a una propiedad de algo cuyo tipo es desconocido al momento de definición de la función (en este caso, el argumento algo).

Un feature de static-eval que seguramente utilicen prácticamente todas las aplicaciones que usan la librería es que se pueden pasar algunas variables que se desea que estén disponibles dentro de la expresión. Por ejemplo, al principio del post usé la expresión book.price > 100. En este caso, el código que llama a static-eval le estaría pasando el valor de la variable book.

Esto me produjo otra idea: ¿y si una función toma un argumento con el nombre de una variable ya definida? como no puede conocer el valor real del argumento, quizás use el valor inicial de la variable. Esto sería muy útil para mí. Supongamos que existe una variable book que es un objeto. Entonces la siguiente expresión:

(function(book){return book.constructor})("".sub)

tendría un resultado muy satisfactorio: al momento de definir la función se chequearía si book.constructor es una expresión válida. Como book es inicialmente un objeto (cuyo typeof es object) y no una función, esto se consideraría una expresión válida y la función sería creada. Al momento de llamar a esta función, sin embargo, book sí será una función y entonces retornará el function constructor.

Lamentablemente, esto tampoco funcionó ya que el autor del fix consideró este caso. Al momento analizar el cuerpo de una función, el valor de todos los argumentos se fija en null, sobreescribiendo el valor de las variables iniciales. Este es un fragmento del código que hace eso:

node.params.forEach(function(key) {
    if(key.type == 'Identifier'){
      vars[key.name] = null;
    }
});

Este código toma el nodo del AST que define la función, itera por cada uno de sus parámetros cuyo tipo sea Identifier, obtiene su nombre y setea a null el atributo de vars con ese nombre. Si bien el código parece correcto, tiene una falla bastante común al programar, que es que no está contemplando todos los casos posibles. ¿qué pasa si key.type es algo raro y tiene un valor diferente de Identifier? en vez de hacer algo prudente como decir "no sé qué es esto así que mejor voy a bloquear a la función" (como si fuera una whitelist), ignora el parámetro y sigue con los demás (como en una blacklist). Esto significa que si logro que el nodo que representa a un argumento de mi función tenga un type distinto de Identifier, el valor de la variable no se pisaría, por lo que seguiría con el valor inicial. Acá empecé a tener seguridad de que encontré algo importante. Solamente me faltaba ver como poner el key.type a algo diferente.

Como comenté anteriormente, static-eval usa la librería esprima para parsear el código de la expresión que le pasemos. Según indica su página, esprima es un parser que soporta correctamente el estándar ECMAScript 2016. ECMAScript viene a ser algo así como un dialecto de JavaScript con más features, que hace que su sintaxis sea más cómoda1.

Un feature que se agregó a ECMAScript es el de function parameter destructuring. Con este feature, el siguiente código ECMAScript ahora es válido:

function nombreCompleto({nombre, apellido}){
    return nombre + " " + apellido;
}
console.log(nombreCompleto({nombre: "John", apellido: "McCarthy"}))

Las llaves adentro de los parámetros de la función indican que la función no toma dos argumentos nombre y apellido, sino solo uno que es un objeto que debe tener las propiedades nombre y apellido. El código anterior es equivalente a esto:

function nombreCompleto(individuo){
    return individuo.nombre + " " + individuo.apellido;
}
console.log(nombreCompleto({nombre: "John", apellido: "McCarthy"}))

Si vemos el AST que genera esprima (yo lo hice usando esta página), nos vamos a llevar una agradable sorpresa (quizás no tan sorprendente):

Resultado de parsear la función que usa parameter destructuring

Efectivamente, esta nueva sintaxis hace que el argumento tenga un type distinto a Identifier, por lo que static-eval no lo va a tener en cuenta a la hora de sobrescribir las variables. De esta forma, al evaluar

(function({book}){return book.constructor})({book:"".sub})

static-eval usa el valor inicial de book, que es un objeto. Entonces autoriza la creación de la función, aunque cuando esta se llama, book pasa a ser una función y retorna su constructor. Ya tengo el bypass!

La expresión anterior devuelve el function constructor, así que solo falta llamarlo para crear una función maliciosa, y después llamar a la función creada:

(function({book}){return book.constructor})({book:"".sub})("console.log(global.process.mainModule.constructor._load(\"child_process\").execSync(\"id\").toString())")()

Probé ejecutar esta expresión localmente con la versión fixeada de static-eval, y obtuve lo que estaba esperando:

Exploit final funcionando

Efectivamente, encontré un bypass a la librería static-eval que me permitió conseguir ejecución de comandos en la máquina que lo ejecute. La única condición requerida es conocer el nombre de una variable definida cuyo valor no sea una función, y que tenga una propiedad constructor. Tanto strings, números, arrays y objetos cumplen esta propiedad, por lo debería ser fácil lograr que se cumpla esta condición. Solamente me faltaba usar esta técnica en el sitio que estaba testeando, lograr una PoC del RCE y reclamar mi dinero, bastante sencillo. ¿o quizás no?

Descubriendo que el exploit no servía en mi target

Desgraciadamente, no. Después de haber hecho todo este trabajo y encontrar un bypass elegante y funcional, me di cuenta de que este no iba a servir en el sitio que estaba testeando. La única condición que el exploit requería era que se tenga el nombre de una variable cuyo valor no sea una función, por lo que resulta lógico asumir que no pude obtener esto y así hacer funcionar mi técnica. Sin embargo, la razón por la que no funcionó es todavía más ridícula.

Para dar un poco de contexto, el sitio no usaba static-eval directamente, sino que lo hacía a través de la librería de npm jsonpath. JSONPath es un query language con un propósito similar a XPATH, pero para documentos JSON. Fue publicado inicialmente en 2007 en este artículo.

Tras familiarizarme con JSONPath me di cuenta de que se trata de un proyecto bastante pobre, sin una especificación clara acerca de cómo debería funcionar. La mayoría de las features que implementa parecen haberse desarrollado de forma improvisada, sin pensar si se estaba tomando una buena decisión al incluirlas. Es una lástima que el ecosistema de NodeJS esté lleno de librerías como esta.

JSONPath tiene un feature al que llama filter expressions, que permite filtrar documentos que cumplan una cierta expresión. Un ejemplo es $.store.book[?(@.price < 10)].title, que busca los libros que salgan menos de $10, y retorna el título de cada uno de ellos. En el caso de la librería que mencioné anteriormente, la expresión entre paréntesis se evalúa utilizando static-eval. El sitio permitía que le pase una expresión de JSONPath que parseaba con esa misma librería, por lo que el RCE era ya evidente.

Si nos ponemos a ver detalladamente el JSONPath anterior, vemos que la expresión que se le pasa a static-eval es @.price < 10. Según la documentación, @ es una variable que contiene información del documento (generalmente es un objeto, por lo que cumple la condición que necesita mi técnica para funcionar). Desgraciadamente, al autor de JSONPath path se le ocurrió llamarle @ a esta variable. Y resulta que según la especificación de ECMAScript, @ no es un nombre de variable válido. Es más, para que la librería funcione correctamente, tuvieron que hacer una cosa horrible que es parchear el código de esprima para que acepte @ como nombre de variable.

Cuando se crea una función anónima en static-eval, esta se mete adentro de otra función que toma como argumentos las variables inicialmente definidas. Es decir, que si creo funciones anónimas dentro de un filter expression de JSONPath, se va a crear una función que toma como argumento una variable @. Esto se hace directamente llamando al function constructor, sin tener en cuenta el parche que le hicieron a esprima, por lo que crear esa función tira un error que no puedo evitar. Esto es un bug en la librería, que hace que no pueda usar funciones dentro de filter expressions (ni benignas ni maliciosas). Y por culpa de esto, mi forma de bypassear el sandbox no funciona en este contexto.

Solamente por culpa de la pésima decisión de llamar @ a una variable en una librería utilizada principalmente en JS, donde @ no es un nombre de variable válido en JS, no fui capaz de encontrar un RCE en el sitio y obtener una recompensa importante, que seguramente no hubiese bajado de las cuatro cifras. Por qué no la habrá llamado _ (que sí es un nombre válido), document o pepito!!! Esta vez me tendré que conformar solamente con haber descubierto una vulnerabilidad en la librería y haber aprendido un montón sobre JavaScript.

Conclusiones

Si bien no pude obtener el bounty que estaba esperando, pasé un buen rato jugando con esta librería, y lo que aprendí lo pude utilizar para bypassear otro tipo entornos de JS restringidos y obtener una recompensa económica. Próximamente espero poder publicar también esta otra investigación.

Quiero mencionar una vez más el excelente trabajo previo hecho por Matt Austin acerca de static-eval. Sin este material quizás no hubiese encontrado esta nueva vulnerabilidad.

Como recomendación general a la hora de testear un sistema, siempre suena interesante tratar de replicar y aislar un feature de este en un entorno local que podamos controlar totalmente. En mi caso, me armé una instancia de Docker con la librería static-eval para probar bypassear el sandbox. Mi problema fue que me quedé exclusivamente jugando con esta instancia en vez de probar lo que iba descubriendo en el sitio real. Si hubiese hecho esto antes, quizás me habría dado cuenta de que lo que estaba haciendo no iba a funcionar en el sitio y me hubiese movido a otra cosa. La lección aprendida es que no hay que abstraerse tanto sobre un sistema, y que no hay que probar lo que encontré en este sistema recién al final.

Finalmente, si están auditando un sitio que tenga un sistema similar para evaluar expresiones dentro de una sandbox, definitivamente recomiendo que se queden algún rato largo tratando de romperlo. Sería muy raro encontrar un sistema de sandbox que no tenga vulnerabilidades, especialmente si se trata de lenguajes dinámicos y llenos de features como pueden ser JavaScript, Python o Ruby. Y cuando estas vulnerabilidades son explotadas, generalmente tienen un impacto crítico en la aplicación que las contenga.

Espero que el post les haya resultado útil. Saludos!

Extra: Cronología del bug


  1. Cabe aclarar que esto es una definición bastante vaga y probablemente incorrecta sobre lo que es ECMAScript en realidad. Es simplemente lo que yo siento que es. Mi indiferencia hacia el ecosistema de JavaScript hace que ni me moleste en buscar una definición más precisa. 

Licencia para Hackear está de vuelta!

Ya hace casi 4 años que no publico nada en mi blog. Un poco más si contamos solamente artículos de mi autoría. Bastantes cosas camiaron desde ese momento, en lo personal conseguí un trabajo bastante demandante y arranqué la facultad por lo que mi tiempo libre se vio considerablemente reducido.

Siempre estuve con ganas de retomar el blog en algún momento, aunque no sabía cuando. Hace unos meses me di cuenta de que prácticamente todo el material técnico que leía estaba escrito en inglés. Si bien ahora me llevo bastante bien con ese idioma, no lo hacía cuando arranqué en el mundo de la seguridad informática, y recuerdo lo complicado que era encontrar información de calidad escrita en español. La mayoría de blogs en español que seguía también dejaron de publicar cosas, o se convirtieron en simples propagandas de los productos de Telefónica. Por esto me decidí a retomar el blog de una vez por todas, quizás con contenido diferente al anterior pero manteniendo la idea de publicar contenido técnico, en español y libre de empresas tratando de vender un producto.

La idea en esta nueva etapa es tratar de que todo lo que se publique esté disponible en español. En caso de que sea alguna publicación importante también puede haber una traducción al inglés. Al igual que en la primera etapa, el contenido será principalmente sobre seguridad informática y programación. En particular, ya tengo artículos pensados sobre vulnerabilidades en librerías de NodeJS, soluciones a retos de CTFs y charlas de conferencias.

Algo sobre lo que no tengo buenos recuerdos es tener que usar el editor de Wordpress para redactar los artículos. Por esto voy a cambiar la plataforma que uso por un generador de sitios estáticos. El nuevo material que publique va a estar disponible en licenciaparahackear.github.io, no en el blog viejo. El material viejo va a seguir disponible solamente en licenciaparahackear.wordpress.com, al menos hasta que encuentre la forma de exportar correctamente los posts de un Wordpress a un sitio estático.

Estén atentos, porque pronto se viene la primera publicación de esta nueva etapa, y se viene con todo.

Saludos!