De Prototipo a Root
Prototype Pollution → RCE
// OBJETIVO
Encadenar la contaminación de Object.prototype con un motor de plantillas que compila con new Function, para ejecutar código en el servidor del lab y leer la flag.
// ZONA DE ATAQUE
Este reto ejecuta código del lado servidor, así que no corre en el sitio. Descargá el lab y ejecutalo aislado en tu máquina:
GitHub no deja bajar una carpeta suelta, así que el .zip trae todos los labs: el de este reto es prototype-pollution-rce/. Si usás git:
git clone https://github.com/Ismaeldevs/jsforge-labs
cd jsforge-labs/prototype-pollution-rce
docker compose upAbrí http://localhost:8080, resolvé el reto y volvé acá a enviar la flag. Nunca expongas el contenedor a internet.
// CONSULTÁ
Pistas
Código vulnerable
La misma primitiva del reto anterior (un deep merge que sigue __proto__) ahora
alcanza un sink que compila código. El motor de plantillas construye sus
opciones como un objeto fresco, así que hereda cualquier sourceURL contaminado, y
lo concatena dentro de la función que crea con new Function:
function compile(source, options) {
const opts = options || {};
let body = /* ...arma __out a partir de la plantilla... */;
// El sourceURL se pega como comentario al inicio del cuerpo, sin sanear.
const header = '//# sourceURL=' + (opts.sourceURL || 'jsforge-template') + '\n';
return new Function('data', header + body);
}
app.post('/api/render', (req, res) => {
const options = {}; // ← hereda sourceURL del prototipo
const render = compile(req.body.template, options);
res.json({ output: render(req.body.data) });
});
Un body { "__proto__": { "sourceURL": "\n return process.env.FLAG //" } } deja
({}).sourceURL con un salto de línea seguido de código. Al compilar, el \n
cierra el comentario //# y el return del atacante se ejecuta dentro de la
función: ejecución de código en el proceso. Es la clase de CVE-2021-23337
(lodash.template).
Remediación
Cortá la cadena en sus dos eslabones: la primitiva y el sink.
- deepMerge(settings, req.body);
+ deepMerge(settings, req.body); // endurecido: descarta __proto__/constructor/prototype
+
- const options = {};
+ const options = Object.assign(Object.create(null), SAFE_DEFAULTS, opcionesPropias);
const render = compile(req.body.template, options);
Por qué funciona: una vez que el merge no puede tocar Object.prototype, el
sink deja de heredar opciones ajenas; y construir las opciones sobre
Object.create(null) con defaults explícitos garantiza que ninguna propiedad
provenga del prototipo. Sin una sourceURL controlada no hay string del atacante
que termine dentro de la función compilada, y la cadena hacia RCE se rompe en su
primer eslabón. Como refuerzo, no interpoles opciones dentro de código que compilás
con new Function.
Reto descargable. Permite ejecución de código: nunca lo expongas a internet. Descargá el lab Docker y corrélo aislado en tu propia máquina.