Hackers compitiendo en el CTF de DEF CON 17 frente a sus notebooksFoto: Nate Grigg, CC BY 2.0, vía Wikimedia Commons

Si tenés un servidor con PHP, esta semana te toca. Y no importa si usás WordPress, Laravel o un sistema hecho a mano en 2009: la falla está en PHP mismo, antes de que llegue a correr tu código.

El 21 de octubre salieron los parches para PHP2Shell, dos vulnerabilidades críticas en el núcleo del lenguaje: CVE-2026-109618 y CVE-2026-109619. Ejecución remota de código, sin autenticación, CVSS 9.2. Los detalles se filtraron antes de tiempo y hubo intentos de explotación desde el 17/10, o sea, cuatro días antes de que existiera el parche.

A los que están sufriendo con cómo parchear: con Fundamenta acabamos de publicar una guía de parcheo completa, con comandos para cada distro: PHP2Shell: cómo parchear y mitigar. Acá te dejo la versión corta y un par de cosas que me encontré parcheando servidores estos días.

Qué se rompe

  • CVE-2026-109618: cuando PHP se queda sin memoria, el manejo del error puede cortar la ejecución con los datos internos del pedido a medio inicializar.
  • CVE-2026-109619: estado interno de un pedido que sobrevive después de que se liberó la memoria que lo respaldaba (un use-after-free de manual). El camino confirmado pasa por session.upload_progress.

Afecta a todo PHP que atienda pedidos web: PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP y LiteSpeed. Con mod_php es donde peor está.

Paso 1: Parchear (no hay otra)

No hay mitigación que cierre todo. Actualizar es la única solución completa. Las versiones corregidas:

  • PHP 8.5.12
  • PHP 8.4.27
  • PHP 8.3.36
  • PHP 8.2.35 (8.2 muere el 31/12/2026, andá planificando)

¿Seguís en 8.1, 8.0 o 7.4? El proyecto PHP ya no los parchea. Hay backports, pero los tenés que pedir a tu distro o a tu proveedor. Y es buen momento para dejar de patear la migración.

Debian / Ubuntu

Ojo con esto: las distros portan el arreglo sin cambiar el número de versión. Así que php -v te puede decir 8.3.6 y estar parcheado igual. Fijate en el changelog del paquete (en Ubuntu es el USN-8948-1).

sudo apt update
sudo apt install --only-upgrade 'php8.*'
apt changelog php8.3-fpm | grep -m3 CVE-2026-1096    # cambiá 8.3 por tu versión
sudo systemctl restart php8.3-fpm                    # o apache2 si usás mod_php

RHEL, Rocky, Alma

sudo dnf upgrade 'php*'
sudo systemctl restart php-fpm httpd

Docker

Rebuild sobre la imagen oficial actualizada (docker pull php:8.3-fpm o la que uses) y redeploy. El host parcheado no te salva si el contenedor sigue corriendo la imagen vieja.

Hosting compartido (cPanel)

Este blog vive en un hosting compartido con cPanel, así que lo probé en carne propia. Sin root no podés actualizar nada, pero sí podés verificar. Si tenés SSH:

rpm -q --changelog ea-php83-php-cli | head -5

Si aparecen los dos CVE, tu proveedor ya parcheó. En mi caso cPanel había sacado el 8.3.36 el 20/10 y hasta backporteó el fix a ea-php80, que está EOL. Si en el tuyo no aparece, pasá a las mitigaciones de abajo y abrile un ticket al proveedor.

Después de actualizar

  • Reiniciá todos los workers (FPM, Apache, LiteSpeed). Un proceso vivo sigue corriendo el binario viejo en memoria.
  • Verificá la versión que atiende la web, no la de la consola. Suelen ser binarios distintos. Un archivo temporal con <?php echo PHP_VERSION;, lo llamás desde el navegador y lo borrás enseguida.
  • Si aplicaste mitigaciones temporales (sobre todo memory_limit), revertilas una vez confirmado el parche.

Lo de consola vs. web no es teoría. En el hosting de este blog el php de consola es 8.2 y la web corre 8.3 por FPM. Dos binarios distintos, dos versiones distintas. Si hubiera mirado sólo php -v me quedaba con un dato que no tenía nada que ver con lo que estaba expuesto.

Paso 2: Mitigar si todavía no podés parchear

Nada de esto cierra el agujero. Achica la superficie y te compra tiempo hasta el parche.

Apagar upload_progress (CVE-2026-109619)

Esto se tiene que aplicar antes de que PHP procese el pedido: en php.ini, en el pool de FPM o en el vhost.

; php.ini
session.upload_progress.enabled = 0

; pool de PHP-FPM (www.conf)
php_admin_flag[session.upload_progress.enabled] = off

# Apache con mod_php, en el vhost
php_admin_flag session.upload_progress.enabled off

Confieso un error: lo primero que hice fue meterlo en el .user.ini del hosting. Probé con ini_get(), devolvía 0, festejé. No sirve. El .user.ini de FPM se aplica tarde, cuando la parte vulnerable del pedido ya pasó. Lo mismo con ini_set(). Y php_admin_flag en un .htaccess te tira un 500 directamente: va en el vhost.

En PHP 8.5 sumale fatal_error_backtraces = 0.

Achicar los pedidos (CVE-2026-109618)

post_max_size = 8M          ; 32M si la aplicación recibe archivos
upload_max_filesize = 6M    ; siempre por debajo de post_max_size

Y reforzalo en el servidor web, que corta antes de que el pedido llegue a PHP: LimitRequestBody en Apache, client_max_body_size en Nginx.

El aviso oficial también propone memory_limit = -1. Ojo: sin límite, un solo pedido se puede comer toda la RAM del server. Si lo aplicás, poné un límite por cgroup o contenedor que mate sólo al worker afectado. Y no uses RLIMIT_AS ni RLIMIT_DATA, que justamente disparan la falla.

Bloquear multipart donde no se suben archivos

# Apache: bloquea multipart salvo en las rutas que suben archivos
RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteCond %{REQUEST_URI} !^/(admin|api/upload)
RewriteRule ^ - [F]

Importante: muchos formularios mandan FormData por JavaScript aunque no tengan archivos, y eso viaja como multipart. Formularios de contacto, carritos, puntos de venta: en las primeras pruebas se rompieron todos. Antes de bloquear una ruta, buscá todos los new FormData del frontend y probá cada formulario. Si estás detrás de Cloudflare o similar, la regla en el borde es todavía mejor.

Paso 3: ¿Ya te tocaron?

Hubo explotación antes del parche, así que actualizar no alcanza. Hay que mirar para atrás desde el 17/10.

# PHP nuevos en carpetas de subidas o caché
find /var/www -name '*.php' -newermt 2026-10-17 \( -path '*upload*' -o -path '*cache*' -o -path '*tmp*' \)

# patrones típicos de webshell
grep -rlE 'eval\(base64_decode|assert\(\$_|system\(\$_|passthru\(\$_' /var/www --include=*.php

# workers que se caen: puede ser un intento
grep -iE 'segfault|SIGSEGV|SIGBUS' /var/log/php*-fpm.log /var/log/apache2/error.log

# pedidos con el campo de progreso de subida
zgrep -i 'PHP_SESSION_UPLOAD_PROGRESS' /var/log/apache2/access.log*

El grep de webshells en WordPress te va a marcar archivos del core (kses.php, file.php). No te asustes: corré wp core verify-checksums y si da OK son falsos positivos.

Mirá también cron, usuarios nuevos, authorized_keys y conexiones salientes raras. Y si encontrás algo, no lo borres sin copiarlo antes: es la evidencia para entender qué pasó.

Bonus: el auto-update que rompió el cron

Revisando el hosting de este blog me encontré otra cosa. Contact Form 7 se auto-actualizó esta madrugada y la versión nueva exige PHP 8.3. La web anda porque corre 8.3, pero el cron de wp-cron.php usaba el php de consola, que es 8.2. Resultado: fatal error en cada corrida y nadie se enteraba. ¿Te pasó? Revisá con qué binario corren tus crons.

Resumen

  • Hoy: PHP a 8.2.35, 8.3.36, 8.4.27 u 8.5.12 (o el paquete corregido de tu distro) y reiniciá todos los workers.
  • Si no podés: session.upload_progress.enabled = 0 donde corresponde (no en .user.ini), límites de tamaño bajos y bloqueo de multipart probado ruta por ruta.
  • En los dos casos: buscá indicios de compromiso desde el 17/10.

La guía completa, con más detalle por plataforma, está en Fundamenta. Si necesitás una mano para actualizar, mitigar o revisar un servidor, escribinos a info@fundamenta.ar.

Foto: equipo compitiendo en el CTF de DEF CON 17, por Nate Grigg, CC BY 2.0, vía Wikimedia Commons.

Por Jeremías Palazzesi

Solucionador de Problemas Senior!. No podés con algo?, probá conmigo!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *