{"id":2198,"date":"2026-10-22T13:22:19","date_gmt":"2026-10-22T16:22:19","guid":{"rendered":"https:\/\/www.nerdadas.com\/blog\/php2shell-como-parchear-y-mitigar\/"},"modified":"2026-10-22T13:22:19","modified_gmt":"2026-10-22T16:22:19","slug":"php2shell-como-parchear-y-mitigar","status":"publish","type":"post","link":"https:\/\/www.nerdadas.com\/blog\/php2shell-como-parchear-y-mitigar\/","title":{"rendered":"PHP2Shell: c\u00f3mo parchear y mitigar (y lo que me encontr\u00e9 en el camino)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Si ten\u00e9s un servidor con PHP, esta semana te toca. Y no importa si us\u00e1s WordPress, Laravel o un sistema hecho a mano en 2009: <strong>la falla est\u00e1 en PHP mismo<\/strong>, antes de que llegue a correr tu c\u00f3digo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El 21 de octubre salieron los parches para <strong>PHP2Shell<\/strong>, dos vulnerabilidades cr\u00edticas en el n\u00facleo del lenguaje: <strong>CVE-2026-109618<\/strong> y <strong>CVE-2026-109619<\/strong>. Ejecuci\u00f3n remota de c\u00f3digo, sin autenticaci\u00f3n, CVSS 9.2. Los detalles se filtraron antes de tiempo y hubo intentos de explotaci\u00f3n desde el 17\/10, o sea, cuatro d\u00edas antes de que existiera el parche.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A los que est\u00e1n sufriendo con c\u00f3mo parchear: con Fundamenta acabamos de publicar una gu\u00eda de parcheo completa, con comandos para cada distro: <a href=\"https:\/\/www.fundamenta.ar\/blog\/php2shell-como-parchear-y-mitigar\">PHP2Shell: c\u00f3mo parchear y mitigar<\/a>. Ac\u00e1 te dejo la versi\u00f3n corta y un par de cosas que me encontr\u00e9 parcheando servidores estos d\u00edas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 se rompe<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>CVE-2026-109618<\/strong>: cuando PHP se queda sin memoria, el manejo del error puede cortar la ejecuci\u00f3n con los datos internos del pedido a medio inicializar.<\/li>\n\n\n\n<li><strong>CVE-2026-109619<\/strong>: estado interno de un pedido que sobrevive despu\u00e9s de que se liber\u00f3 la memoria que lo respaldaba (un use-after-free de manual). El camino confirmado pasa por <code>session.upload_progress<\/code>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e1.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Paso 1: Parchear (no hay otra)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">No hay mitigaci\u00f3n que cierre todo. <strong>Actualizar es la \u00fanica soluci\u00f3n completa.<\/strong> Las versiones corregidas:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>PHP 8.5.12<\/li>\n\n\n\n<li>PHP 8.4.27<\/li>\n\n\n\n<li>PHP 8.3.36<\/li>\n\n\n\n<li>PHP 8.2.35 (8.2 muere el 31\/12\/2026, and\u00e1 planificando)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u00bfSegu\u00eds en 8.1, 8.0 o 7.4? El proyecto PHP ya no los parchea. Hay backports, pero los ten\u00e9s que pedir a tu distro o a tu proveedor. Y es buen momento para dejar de patear la migraci\u00f3n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debian \/ Ubuntu<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ojo con esto: las distros portan el arreglo <strong>sin cambiar el n\u00famero de versi\u00f3n<\/strong>. As\u00ed que <code>php -v<\/code> te puede decir 8.3.6 y estar parcheado igual. Fijate en el changelog del paquete (en Ubuntu es el USN-8948-1).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install --only-upgrade 'php8.*'\napt changelog php8.3-fpm | grep -m3 CVE-2026-1096    # cambi\u00e1 8.3 por tu versi\u00f3n\nsudo systemctl restart php8.3-fpm                    # o apache2 si us\u00e1s mod_php<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">RHEL, Rocky, Alma<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf upgrade 'php*'\nsudo systemctl restart php-fpm httpd<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Docker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rebuild sobre la imagen oficial actualizada (<code>docker pull php:8.3-fpm<\/code> o la que uses) y redeploy. El host parcheado no te salva si el contenedor sigue corriendo la imagen vieja.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Hosting compartido (cPanel)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Este blog vive en un hosting compartido con cPanel, as\u00ed que lo prob\u00e9 en carne propia. Sin root no pod\u00e9s actualizar nada, pero s\u00ed pod\u00e9s verificar. Si ten\u00e9s SSH:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rpm -q --changelog ea-php83-php-cli | head -5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Si aparecen los dos CVE, tu proveedor ya parche\u00f3. En mi caso cPanel hab\u00eda sacado el 8.3.36 el 20\/10 y hasta backporte\u00f3 el fix a <code>ea-php80<\/code>, que est\u00e1 EOL. Si en el tuyo no aparece, pas\u00e1 a las mitigaciones de abajo y abrile un ticket al proveedor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Despu\u00e9s de actualizar<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Reinici\u00e1 todos los workers<\/strong> (FPM, Apache, LiteSpeed). Un proceso vivo sigue corriendo el binario viejo en memoria.<\/li>\n\n\n\n<li><strong>Verific\u00e1 la versi\u00f3n que atiende la web, no la de la consola.<\/strong> Suelen ser binarios distintos. Un archivo temporal con <code>&lt;?php echo PHP_VERSION;<\/code>, lo llam\u00e1s desde el navegador y lo borr\u00e1s enseguida.<\/li>\n\n\n\n<li>Si aplicaste mitigaciones temporales (sobre todo <code>memory_limit<\/code>), revertilas una vez confirmado el parche.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Lo de consola vs. web no es teor\u00eda. En el hosting de este blog el <code>php<\/code> de consola es 8.2 y la web corre 8.3 por FPM. Dos binarios distintos, dos versiones distintas. Si hubiera mirado s\u00f3lo <code>php -v<\/code> me quedaba con un dato que no ten\u00eda nada que ver con lo que estaba expuesto.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Paso 2: Mitigar si todav\u00eda no pod\u00e9s parchear<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nada de esto cierra el agujero. Achica la superficie y te compra tiempo hasta el parche.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Apagar upload_progress (CVE-2026-109619)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Esto se tiene que aplicar <strong>antes<\/strong> de que PHP procese el pedido: en php.ini, en el pool de FPM o en el vhost.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>; php.ini\nsession.upload_progress.enabled = 0\n\n; pool de PHP-FPM (www.conf)\nphp_admin_flag&#91;session.upload_progress.enabled] = off\n\n# Apache con mod_php, en el vhost\nphp_admin_flag session.upload_progress.enabled off<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Confieso un error:<\/strong> lo primero que hice fue meterlo en el <code>.user.ini<\/code> del hosting. Prob\u00e9 con <code>ini_get()<\/code>, devolv\u00eda 0, festej\u00e9. <strong>No sirve.<\/strong> El <code>.user.ini<\/code> de FPM se aplica tarde, cuando la parte vulnerable del pedido ya pas\u00f3. Lo mismo con <code>ini_set()<\/code>. Y <code>php_admin_flag<\/code> en un <code>.htaccess<\/code> te tira un 500 directamente: va en el vhost.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En PHP 8.5 sumale <code>fatal_error_backtraces = 0<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Achicar los pedidos (CVE-2026-109618)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>post_max_size = 8M          ; 32M si la aplicaci\u00f3n recibe archivos\nupload_max_filesize = 6M    ; siempre por debajo de post_max_size<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Y reforzalo en el servidor web, que corta antes de que el pedido llegue a PHP: <code>LimitRequestBody<\/code> en Apache, <code>client_max_body_size<\/code> en Nginx.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El aviso oficial tambi\u00e9n propone <code>memory_limit = -1<\/code>. Ojo: sin l\u00edmite, un solo pedido se puede comer toda la RAM del server. Si lo aplic\u00e1s, pon\u00e9 un l\u00edmite por cgroup o contenedor que mate s\u00f3lo al worker afectado. Y no uses <code>RLIMIT_AS<\/code> ni <code>RLIMIT_DATA<\/code>, que justamente disparan la falla.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Bloquear multipart donde no se suben archivos<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Apache: bloquea multipart salvo en las rutas que suben archivos\nRewriteEngine On\nRewriteCond %{HTTP:Content-Type} multipart\/form-data &#91;NC]\nRewriteCond %{REQUEST_URI} !^\/(admin|api\/upload)\nRewriteRule ^ - &#91;F]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Importante:<\/strong> muchos formularios mandan <code>FormData<\/code> 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\u00e1 todos los <code>new FormData<\/code> del frontend y prob\u00e1 cada formulario. Si est\u00e1s detr\u00e1s de Cloudflare o similar, la regla en el borde es todav\u00eda mejor.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Paso 3: \u00bfYa te tocaron?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hubo explotaci\u00f3n antes del parche, as\u00ed que actualizar no alcanza. <strong>Hay que mirar para atr\u00e1s desde el 17\/10.<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># PHP nuevos en carpetas de subidas o cach\u00e9\nfind \/var\/www -name '*.php' -newermt 2026-10-17 \\( -path '*upload*' -o -path '*cache*' -o -path '*tmp*' \\)\n\n# patrones t\u00edpicos de webshell\ngrep -rlE 'eval\\(base64_decode|assert\\(\\$_|system\\(\\$_|passthru\\(\\$_' \/var\/www --include=*.php\n\n# workers que se caen: puede ser un intento\ngrep -iE 'segfault|SIGSEGV|SIGBUS' \/var\/log\/php*-fpm.log \/var\/log\/apache2\/error.log\n\n# pedidos con el campo de progreso de subida\nzgrep -i 'PHP_SESSION_UPLOAD_PROGRESS' \/var\/log\/apache2\/access.log*<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">El grep de webshells en WordPress te va a marcar archivos del core (<code>kses.php<\/code>, <code>file.php<\/code>). No te asustes: corr\u00e9 <code>wp core verify-checksums<\/code> y si da OK son falsos positivos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mir\u00e1 tambi\u00e9n cron, usuarios nuevos, <code>authorized_keys<\/code> y conexiones salientes raras. Y si encontr\u00e1s algo, <strong>no lo borres sin copiarlo antes<\/strong>: es la evidencia para entender qu\u00e9 pas\u00f3.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bonus: el auto-update que rompi\u00f3 el cron<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Revisando el hosting de este blog me encontr\u00e9 otra cosa. Contact Form 7 se auto-actualiz\u00f3 esta madrugada y la versi\u00f3n nueva exige PHP 8.3. La web anda porque corre 8.3, pero el cron de <code>wp-cron.php<\/code> usaba el <code>php<\/code> de consola, que es 8.2. Resultado: fatal error en cada corrida y nadie se enteraba. \u00bfTe pas\u00f3? Revis\u00e1 con qu\u00e9 binario corren tus crons.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Resumen<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Hoy:<\/strong> PHP a 8.2.35, 8.3.36, 8.4.27 u 8.5.12 (o el paquete corregido de tu distro) y reinici\u00e1 todos los workers.<\/li>\n\n\n\n<li><strong>Si no pod\u00e9s:<\/strong> <code>session.upload_progress.enabled = 0<\/code> donde corresponde (no en <code>.user.ini<\/code>), l\u00edmites de tama\u00f1o bajos y bloqueo de multipart probado ruta por ruta.<\/li>\n\n\n\n<li><strong>En los dos casos:<\/strong> busc\u00e1 indicios de compromiso desde el 17\/10.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">La gu\u00eda completa, con m\u00e1s detalle por plataforma, est\u00e1 en <a href=\"https:\/\/www.fundamenta.ar\/blog\/php2shell-como-parchear-y-mitigar\">Fundamenta<\/a>. Si necesit\u00e1s una mano para actualizar, mitigar o revisar un servidor, escribinos a <a href=\"mailto:info@fundamenta.ar\">info@fundamenta.ar<\/a>.<\/p>\n\n\n\n<p class=\"has-small-font-size wp-block-paragraph\">Foto: equipo compitiendo en el CTF de DEF CON 17, por <a href=\"https:\/\/commons.wikimedia.org\/wiki\/File:DEF_CON_17_CTF_competition.jpg\">Nate Grigg<\/a>, <a href=\"https:\/\/creativecommons.org\/licenses\/by\/2.0\/\">CC BY 2.0<\/a>, v\u00eda Wikimedia Commons.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Dos RCE cr\u00edticas en el n\u00facleo de PHP (CVE-2026-109618 y CVE-2026-109619). C\u00f3mo parchear, c\u00f3mo mitigar si todav\u00eda no pod\u00e9s y c\u00f3mo revisar si ya te tocaron.<\/p>\n","protected":false},"author":1,"featured_media":2197,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1427,1241,161],"tags":[1484,40,1483],"class_list":["post-2198","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ciberseguridad","category-hardening","category-seguridad","tag-cve","tag-php","tag-php2shell"],"_links":{"self":[{"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/posts\/2198","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/comments?post=2198"}],"version-history":[{"count":0,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/posts\/2198\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/media\/2197"}],"wp:attachment":[{"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/media?parent=2198"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/categories?post=2198"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.nerdadas.com\/blog\/wp-json\/wp\/v2\/tags?post=2198"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}