Creo recordar que fue viendo este corto en RRSS (https://www.youtube.com/shorts/KmL9R2bAlxQ), que conocí esta interesante vulnerabilidad llamada SSJI, que viene de las siglas de Server Side JavaScript Injection. A su vez me parece que también por esas fechas vi el siguiente flyer de la gente de Vulnmachines:

En donde anunciaban el lanzamiento de un nuevo reto ¡GRATUITO! en su plataforma. Reto que consistía en explotar esta vulnerabilidad de SSJI (Server Side JavaScript Injection), por tanto esta semana saqué algo de tiempo para intentarlo y la verdad que me quedé fascinado con los detalles de la misma. Os lo intentaré plasmar de la mejor manera que me es posible en esta entrada.

Para acceder al reto podéis registraros en la plataforma de Vulnmachines, en donde también encontraréis otros laboratorios o simplemente podéis acceder a la siguiente URL de la captura:

Antes de empezar con el análisis pongamos un poco de contexto básico teórico (no seré muy profundo dado que aún estoy asimilando algunos conceptos y que lo mejor será que os apoyéis en algunos recursos que os voy compartiendo mediante enlaces):

Entendemos que JavaScript es un lenguaje de programación, que normalmente lo podemos ver asociado a páginas web y que como uno de los objetivos que tiene es la interacción con el navegador del usuario, aquí llegamos al concepto que escucharemos mucho como: ‘del lado del cliente‘.

En el servidor existen componente que se encargan de construir dinámicamente la página del usuario «bajo pedido», en función a los datos que éste inserta y envía. Del lado del cliente, estos datos normalmente pasan por un proceso de validación y control, como que la ejecución es lanzada en un entorno de espacio aislado (Sandbox) y que se rigen a ciertas reglas de origen. Esto del lado del servidor no se comporta de la misma forma y por tanto es posible saltar ciertas restricciones.

Digamos que hasta ahora cuando hablábamos de inyecciones asociadas a JavaScript, en nuestra cabeza se «matcheaba» inmediatamente la palabra XSS (Cross-Site Scripting), esto entendiendo la ejecución del lado del cliente. Pero en los últimos tiempos con la aparición y popularidad de NodeJS, JavaScript se volvió en un lenguaje que también puede tener un papel muy relevante en el backend de las aplicaciones.

Por tanto diremos y concluiremos en este punto que:

La inyección de JavaScript del lado del servidor es la capacidad que tiene un usuario de insertar código especialmente diseñado que será evaluado y tratado por el servidor, por tanto le permitirá al potencial atacante ejecutar código arbitrario en el contexto del servidor y la interacción directa con el sistema de archivos, propiciando (potencialmente) la conducción al compromiso completo de dicho activo.

Bien, dejando de lado este «tostón», veamos como podemos resolver este reto que nos planteaban desde Vulnmachines:

El análisis podemos empezarlo como cualquier otra validación web, observando el comportamiento de la aplicación a nuestras inserciones más básicas:

Sobre el campo de texto que se nos presenta insertamos la letra «a» y observamos que inmediatamente se realizan búsquedas «en caliente». En la parte derecha de dicho formulario tenemos un botón que nos permite elegir si deseamos buscar un ‘First Name‘ o un ‘Last Name‘:

Pero bueno, dejemos de lado el frontend de la aplicación para centrarnos en las peticiones que se generan desde nuestro navegador al servidor web. Aquí observamos las siguiente peticiones:

Se trata de una petición GET en donde entran en escena 2 parámetros: q y searchBy. Por otro lado, en la respuesta del servidor se observa la revelación de la tecnología Express. Recordemos que Express es un framework para Node.js.

Luego será momento de testear el valor que se pueda insertar sobre los parámetros. Arranquemos con el primero (q).

Esto será interesante hacerlo para conocer el comportamiento de la aplicación ante la inserción de una cadena desconocida o que no se espera.

Ya luego centrados en sintaxis propia para este tipo de backend que intuimos tener detrás de la aplicación, emplearemos la siguiente:

Con esta función buscaremos la inserción final del texto que entrecomillemos:

res.end('SSJI')

Según la documentación de Express, básicamente la función res.end() se emplea para finalizar el proceso de respuesta. Veamos el comportamiento de la aplicación en este caso:

No parece que nuestra inserción pueda ser interpretada por el backend de la aplicación, así que dejaremos de lado este primer parámetro.
Veamos como se comporta el parámetro siguiente (searchBy). Para ello nuevamente emplearemos la función res.end(), de la siguiente manera:

res.end('SSJI+Vulnerability')

Aquí la respuesta de la aplicación ya es más reveladora. Prácticamente acabamos de lograr que nuestra función insertada sea interpretada en el backend y por tanto tenemos control sobre la acción que pueda estar realizando el servidor. Hemos demostrado la vulnerabilidad!

Otras funciones que convendría tomar en cuenta y que devuelven este mismo resultado son: res.write(), res.send(), res.json(), res.jsonp().

Bien, ahora que hemos reconocido la vulnerabilidad de SSJI, apoyados en el método require(), vamos a utilizar el modulo nativo de Node.js llamado fs (File System), el mismo que nos permite trabajar con el sistema de archivos del servidor.
Importando el módulo fs conseguiremos disponer de distintos métodos que nos permitirán obtener la lectura de directorios y la lectura de ficheros, entre otras acciones.
Por ejemplo a través del siguiente comando, conseguiremos leer el contenido del directorio actual:

res.end(require('fs').readdirSync('.').toString())

En este caso empleamos un método síncrono (readdirSync()), dado que éste por sus características. bloquea la ejecución de las siguiente líneas de código hasta que la acción del método se haya concluido.

Otras cargas útiles que podemos emplear para este propósito son:

res.send(require('fs').readdirSync('.'))
res.json(require('fs').readdirSync('.'))
res.jsonp(require('fs').readdirSync('.'))

Veamos algunos ejemplos de la efectividad y sus resultados:

Ya luego será momento de leer ficheros concretos del servidor, para ello emplearemos el siguiente payload:

res.json(require('fs').readFileSync('Dockerfile'))

Dado que la función res.json() nos devuelve el resultado de esta forma presentada, adicionalmente tendremos que pasar la salida a un formato legible (no decimal), y para ello emplearemos nuestro chef de cabecera (https://gchq.github.io/CyberChef/):

Observemos otras cargas útiles que también pueden ser empleadas y que ocupan distintas funciones conocidas:

res.end(require('fs').readFileSync('Dockerfile'))
res.write(require('fs').readFileSync('Dockerfile'))
res.send(require('fs').readFileSync('Dockerfile')) 
res.end(require('fs').readFileSync('app.js').toString())

Con esta técnica descrita, finalmente conseguiremos llegar a la flag de este reto y por tanto completarlo de forma exitosa.

res.end(require('fs').readFileSync('app.js'))

Adicionalmente observando el código fuente de la aplicación en backend, conseguiremos reconocer la función vulnerable y que propicia esta debilidad (eval()):

Por último, no quiero desaprovechar la oportunidad para recomendar una vez más esta enorme plataforma llamada Vulnmachines, en esta encontrarás una serie de laboratorios donde poder poner en práctica tus habilidades y pulir (aún más) diversas técnicas.

Yo por mi parte, por aquí os dejo y espero pronto traeros un nuevo laboratorio de esta plataforma, que es tan entretenida. Hasta el siguiente!

Referencias