Volver al Blog

Protección de servidores de juego contra php2shell: runbook de parcheo de 6 horas

Publicado el 23 de octubre de 2026
Protección de servidores de juego contra php2shell: runbook de parcheo de 6 horas Generado con ayuda de IA

En resumen

Protege tu backend de juego contra php2shell con este runbook de parcheo en 6 horas: inventario de runtimes, parcheo, reciclaje de workers y detección

La capa PHP de tu juego es probablemente la parte de tu stack que nadie mira. La tienda web que vende cosméticos. El panel de administración que usa tu community manager. El endpoint de leaderboards heredado que escribiste en 2022 y nunca volviste a tocar. Esa es la superficie que atacan los php2shell.

El 21 de octubre de 2026, Calif.io, Wiz, OpenAI y Quarkslab divulgaron dos vulnerabilidades del runtime de PHP registradas como CVE-2026-109618 y CVE-2026-109619, denominadas colectivamente php2shell. CVSS v4.0 las puntúa con 9.2. Afectan a todas las instalaciones de PHP desde 5.4 en adelante, permiten ejecución remota de código no autenticada, exposición de datos y denegación de servicio, y Cloudflare observó tráfico de ataque en vivo mutando payloads contra clientes reales antes de que siquiera se publicara la divulgación.

Esto no es un problema de un plugin de WordPress. Es un bug de seguridad de memoria en el propio runtime de PHP. Actualizar tu framework, tu CMS o tus dependencias de Composer no sirve de nada. Si operas un backend de juego con cualquier PHP en él, este runbook es para ti.

Qué rompe realmente php2shell

Ambos CVE residen en cómo PHP gestiona la memoria cuando el procesamiento de peticiones se desvía. Normalmente PHP asigna memoria para una petición, la usa y la libera cuando la petición termina. Los bugs aparecen cuando esa ruta de limpieza se ejecuta sobre un estado que nunca fue válido en primer lugar.

CVE-2026-109618: corrupción por falta de memoria

PHP alcanza su límite de memoria en mitad de la petición y aborta antes de que algunas estructuras internas terminen de inicializarse. La rutina de limpieza trata entonces esos datos a medio construir como legítimos. Un atacante que pueda forzar condiciones OOM de forma fiable — cuerpos de petición grandes, entrada profundamente anidada, subidas de archivos sobredimensionadas — tiene la oportunidad de controlar lo que contiene esa memoria inconclusa.

El desencadenante práctico para backends de juego es obvio: cualquier endpoint que acepte un cuerpo sin límite de tamaño. Envío de resultados de partida, subida de capturas de pantalla, metadatos de mods, lotes de telemetría.

CVE-2026-109619: estado global obsoleto entre peticiones

Un error fatal interrumpe una petición, dejando al proceso PHP con referencias a memoria que se libera al final de la petición. Cuando el mismo proceso worker procesa la siguiente petición, puede seguir esas referencias colgantes. Eso es exposición de datos — los datos de sesión de un jugador o las credenciales de la base de datos filtrándose en la respuesta de otro jugador — y, bajo las condiciones adecuadas, ejecución de código.

Este es el bug más insidioso para servidores de juego porque los workers de PHP-FPM son de larga vida por diseño. Un único worker envenenado puede atender cientos de peticiones posteriores antes de ser reciclado. Si ejecutas pm.max_requests = 0 (el valor predeterminado, que significa no reciclar nunca), un worker corrupto puede persistir durante toda la vida del contenedor.

Paso 1: Haz inventario de cada runtime de PHP que tengas

No puedes parchear lo que no has encontrado. La mayoría de los estudios descubren, en pleno incidente, que tres de sus servicios ejecutan PHP y que uno de ellos está en 8.1 con una imagen base construida hace dos años. Ejecuta esto antes de hacer cualquier otra cosa.

#!/usr/bin/env bash
# php2shell exposure inventory (CVE-2026-109618 / CVE-2026-109619)
set -euo pipefail

declare -A FIXED=( [8.5]="8.5.12" [8.4]="8.4.27" [8.3]="8.3.36" [8.2]="8.2.35" )
SERVICES=("game-api" "web-store" "admin-panel" "forums")

for svc in "${SERVICES[@]}"; do
  ver=$(docker compose exec -T "$svc" php -r 'echo PHP_VERSION;' 2>/dev/null | tr -d '[:space:]') || {
    echo "SKIP  $svc (no php binary)"; continue; }

  branch="${ver%.*}"          # "8.3.12" -> "8.3"
  min="${FIXED[$branch]:-}"

  if [[ -z "$min" ]]; then
    echo "EOL   $svc on $ver -> no upstream patch, upgrade required"
  elif php -r 'exit(version_compare($argv[1], $argv[2], ">=") ? 0 : 1);' "$ver" "$min"; then
    echo "OK    $svc on $ver"
  else
    echo "VULN  $svc on $ver (fixed in $min)"
  fi
done

Dos cosas hace este script deliberadamente: marca las ramas EOL por separado y nunca asume que php -v del host refleja el runtime real del contenedor. Si estás en Kubernetes, cambia el docker compose exec por kubectl exec e itera sobre deployments en lugar de nombres de servicio.

Las versiones oficiales corregidas son 8.5.12, 8.4.27, 8.3.36 y 8.2.35. Cualquier versión en 8.1, 8.0, 7.x o 5.x está en fin de vida y nunca recibirá un fix. Estás actualizando, no parcheando.

Paso 2: Parchea el runtime, no la aplicación

El modo de fallo más común en la primera hora de un incidente así es un equipo que actualiza el código de la aplicación, redespliega, ve el CI en verde y asume que ya ha terminado. No es así.

  • PHP-FPM y mod_php: reconstruye la imagen base o sube la versión del paquete. Debian/Ubuntu: apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine: apk upgrade php83. Verifícalo con php -r 'echo PHP_VERSION;' dentro del contenedor en ejecución, no en el host de compilación.
  • FrankenPHP / php-cgi: estos incluyen el runtime, así que necesitas un binario de nueva versión o una imagen base nueva, no una actualización de paquete.
  • Hosting gestionado: si un proveedor controla PHP, abre un ticket de inmediato y pide la referencia del advisory. No aceptes "somos conscientes" como resolución.
  • Imágenes base de Docker: fija por digest, no por tag. php:8.3-fpm puede resolverse a una compilación diferente en el siguiente pull, lo cual es genial para parchear y terrible para la reproducibilidad. Fija el digest después de verificar la versión.

Después de parchear, mata todos los workers existentes. Un reinicio rotativo que deje procesos antiguos vivos en un socket compartido mantiene a workers vulnerables sirviendo tráfico. docker compose restart o kubectl rollout restart — confirma que la hora de inicio del proceso cambió.

Paso 3: Reduce el radio de explosión mientras esperas

Parchear lleva tiempo. Tienes una ventana entre "lo sabemos" y "cada entorno está reconstruido". Úsala.

Limita los cuerpos de petición en el borde

# Shrink the php2shell trigger surface
client_max_body_size 2m;          # game APIs rarely need more than this
client_body_timeout 10s;
client_header_timeout 10s;
large_client_header_buffers 4 8k;

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass php-fpm:9000;
    fastcgi_read_timeout 15s;
    fastcgi_buffering on;
}

Un límite de 2 MB no detiene a un atacante decidido — puede provocar OOM con muchas peticiones pequeñas — pero elimina el desencadenante trivial de una sola petición y no te cuesta nada.

Recicla workers de forma agresiva

; /usr/local/etc/php-fpm.d/zz-php2shell.conf
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 20
pm.max_requests = 500
pm.max_children = 24
request_terminate_timeout = 20s

Estas son las matemáticas del trade-off. A 200 peticiones por segundo entre 24 workers, cada worker gestiona aproximadamente 8.3 req/s. Con pm.max_requests = 500, cada worker se recicla cada ~60 segundos. Eso limita cuánto tiempo puede servir tráfico un proceso corrupto por CVE-2026-109619, pero también significa 24 procesos creados por minuto — una sobrecarga medible en frameworks con cold starts pesados.

Subir memory_limit de 128M a 256M reduce la frecuencia con la que alcanzas OOM (menos disparadores de CVE-2026-109618) pero duplica la memoria en el peor de los casos: 24 children × 256M = 6 GB de margen que ahora necesitas en el nodo. Si tu nodo tiene 4 GB, baja pm.max_children en lugar de fingir que la memoria existe.

Nada de esto es una solución. Te compra horas. Trátalo como triaje.

Paso 4: Detecta si ya te han golpeado

La divulgación no equivale a la primera explotación. Se observó tráfico de ataque antes de la fecha de lanzamiento oficial. Asume que te sondearon.

# 1. OOM aborts — strongest single signal for CVE-2026-109618
grep -c "Allowed memory size" /var/log/php-fpm/error.log

# 2. Worker crashes — signal for both CVEs
journalctl -u php8.3-fpm --since "48 hours ago" \
  | grep -Ei "segfault|SIGSEGV|exited on signal 11"

# 3. Which paths are generating 500s
awk '$9 == 500 {print $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

# 4. Same source IP hitting the same 500-ing path repeatedly
awk '$9 == 500 {print $1, $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

Primero establece la línea base. Si tu log de PHP-FPM ya tenía 40 abortos OOM al día por un endpoint de exportación que consume mucha memoria, ese número es ruido. Lo que buscas es un cambio en la tasa — un pico de salidas con signal 11, una nueva ruta apareciendo en la lista de 500, o una única IP generando decenas de fallos contra un endpoint.

Si encuentras evidencia de explotación exitosa, trátalo como una brecha completa: rota las credenciales de la base de datos, las claves de firma de sesión, los secretos del proveedor de pagos y cualquier clave de API accesible desde el proceso PHP. El playbook para eso no es específico de PHP — la misma disciplina que permite a un estudio diseñar backends de juego que sobrevivan a compromisos aplica aquí, y comienza asumiendo que el atacante tuvo ejecución de código, no solo acceso de lectura.

Si tu backend ya está cayendo peticiones y no estás seguro de si es carga o un ataque, el orden de triaje de caídas de este protocolo de diagnóstico de congelación de servidor aplica: aísla la capa que falla antes de tocar la configuración.

Por qué fallaron las reglas WAF basadas solo en la petición

El informe de Cloudflare contiene la lección de ingeniería más transferible de todo el incidente. Sus protecciones iniciales se basaban en la petición, y los atacantes respondieron mutando payloads — probando qué variantes coincidían y cuáles se colaban, usando la señal de coincidencia/no coincidencia como un oráculo.

Los datos de la petición por sí solos no podían separar el tráfico de explotación del tráfico legítimo, porque un POST grande con arrays anidados es también lo que envía un cliente de juego normal cuando envía un resumen de partida. La señal que resolvió la ambigüedad fue la respuesta: ¿devolvió el origen un 500 con un trace de error fatal de PHP? Esa combinación — petición sospechosa más respuesta diagnóstica — es de alta confianza. La petición sola no lo es.

Cloudflare lanzó esto como "full transaction detection", correlacionando petición y respuesta dentro de la misma transacción HTTP y bloqueando la respuesta antes de que llegue al solicitante. Si ejecutas tu propio edge o proxy inverso, merece la pena copiar el patrón:

def is_probable_php2shell(resp) -> bool:
    """Correlate request shape with origin response, not request alone."""
    body = resp.body[:4096]
    return (
        resp.status == 500
        and b"Fatal error" in body
        and resp.origin_latency_ms < 50      # exploit aborts fast
        and b"Allowed memory size" in body   # OOM path = 109618 signature
    )

La lección se generaliza más allá de este CVE: cualquier sistema de detección que solo vea tráfico entrante será evadido por un atacante que pueda ver el resultado saliente. Registra las respuestas. Correlaciónalas. Es la diferencia entre una regla que se sostiene y una regla que se convierte en un rompecabezas.

Mejores prácticas

  1. Parchea con una programación, no según la divulgación. La brecha entre "CVE publicado" y "exploit en libertad" se mide ahora en horas, a veces negativas. Reconstruye las imágenes base de PHP semanalmente incluso cuando no se anuncie nada, y conserva una ruta de rollback.
  2. Nunca ejecutes pm.max_requests = 0 en producción. La vida ilimitada del worker convierte cada bug de seguridad de memoria en una posición persistente. 500–1000 es un valor por defecto razonable para APIs de juego.
  3. Limita todos los cuerpos de petición en el borde. 2 MB para APIs JSON, 10 MB para subidas, y rechaza cualquier cosa sin un content type coincidente. Esto elimina clases enteras de exploits de presión de memoria antes de que lleguen a PHP.
  4. Registra los cuerpos de respuesta para errores 5xx, con muestreo. No puedes construir detección de transacción completa si tus respuestas de origen son invisibles para tu stack de observabilidad. Muestrea el 10% y conserva 7 días.
  5. Mantén un inventario de cada runtime que operes. No solo PHP — Node, Python, Go, cualquier cosa que ejecute tu capa web. Los estudios que respondieron a php2shell en menos de una hora tenían un catálogo de servicios. Los que tardaron un día estaban haciendo grep a los Dockerfiles.

La lección más grande: tu capa web es la superficie de ataque

php2shell es un recordatorio de que los backends de juego acumulan desechos. Lanzas un panel de administración en PHP en el primer año porque era rápido de construir. En el tercer año, sigue ahí, todavía en 8.1, todavía con una credencial de base de datos con acceso de escritura a la tabla de jugadores — y es lo que acaba siendo comprometido, no tu binario de servidor de juego endurecido.

El arreglo estructural es reducir cuánto runtime posees. Si la única razón por la que operas una capa web PHP es para gestionar cuentas de jugadores, guardados en la nube y leaderboards, esas son exactamente las capacidades que horizOn expone como APIs gestionadas — autenticación de usuario final con sesiones vinculadas a la cuenta, Cloud Save con lecturas y escrituras conscientes de revisión donde el cliente resuelve conflictos, leaderboards y configuración remota. Sin pool de PHP-FPM que parchear a las 3 a.m. porque no hay pool de PHP-FPM.

Eso no es un argumento de que los servicios gestionados sean inmunes a las vulnerabilidades. Es un argumento de que cada runtime que operas es un runtime que debes inventariar, parchear y monitorizar — y de que la mayoría de los estudios tienen más de los que creen.

Haz esto ahora

Si te llevas una sola acción de este artículo, que sea el script de inventario del Paso 1. Ejecútalo contra cada entorno — producción, staging, el servidor demo olvidado de esa presentación al publisher. Casi con total seguridad encontrarás un runtime que olvidaste, y casi con total seguridad estará en una rama EOL.

Después parchea, recicla los workers y deja las reglas de borde en su sitio. Las reglas WAF y los límites de cuerpo son defensa en profundidad, no un sustituto del fix del proveedor — el advisory de PHP es la fuente de verdad y es lo que realmente cierra el agujero.

Si el inventario revela más PHP del que quieres mantener de aquí en adelante, horizOn cubre la capa de cuentas, guardados y leaderboards como servicios gestionados, que es la forma más rápida de reducir la superficie por la que eres responsable de parchear. En cualquier caso: inventario primero, parcheo después, y deja de asumir que la parte aburrida de tu stack es la parte segura.


Fuente: Cloudflare despliega protecciones contra la vulnerabilidad php2shell