Cuando sale una vulnerabilidad grave, como PHP2Shell en octubre de 2026, lo primero es actualizar. Lo segundo es averiguar si atacaron tu WordPress antes de que lo hicieras. La respuesta está en los registros del servidor: unos archivos de texto donde Apache, Nginx y PHP anotan cada visita y cada error. Esta guía explica, paso a paso, dónde están, cómo leerlos con unas pocas órdenes y cómo distinguir el ruido de siempre de lo que de verdad debería preocuparte.
mitienda.test, y le lanzamos los ataques típicos: fuerza bruta contra el inicio de sesión, una red de bots contra xmlrpc.php y un escáner buscando archivos. Las direcciones IP son de las reservadas para documentación (203.0.113.x): en tu servidor verás otras. Todas las órdenes se ejecutaron como un usuario normal con sudo, igual que lo harías tú.Antes de empezar
- Necesitas entrar en el servidor por SSH con un usuario que pueda usar
sudo. En una instancia EC2 de Amazon esec2-user; en Ubuntu,ubuntu. Si no sabes cómo, empieza por la guía de SSH. - Si tienes un alojamiento compartido (sin acceso por SSH), busca en el panel de control un apartado de registros de acceso o «logs» que permita descargarlos. Si no lo hay, pídeselos a tu proveedor: las mismas órdenes funcionan en tu ordenador con el archivo descargado (en macOS y Linux, tal cual; en Windows, desde WSL).
- Las órdenes solo leen. Ninguna de las de esta guía cambia nada en el servidor hasta el apartado de cerrar puertas, que es opcional.
Qué registro anota qué
Una petición pasa por varias piezas, y cada una apunta cosas distintas. Apache (o Nginx) anota todas las peticiones en el registro de acceso, y en el registro de errores solo las que fallan. Si la petición es para un archivo PHP, se la pasa a PHP-FPM, que tiene sus propios registros para los errores de PHP y para los procesos que se caen.
Paso 1: encontrar los registros
Dependen del sistema y de cómo se configuró el sitio. Los lugares habituales son estos:
- Amazon Linux, Red Hat, Rocky, Alma o Fedora (Apache se llama
httpd): la carpeta/var/log/httpd/. Si el sitio tiene su propio host virtual, sus registros suelen llevar el nombre del dominio, comotusitio.com-access.log, y los genéricosaccess_logyerror_logpueden estar casi vacíos. Si usas HTTPS, puede haber un par aparte consslen el nombre: es donde estará casi todo el tráfico. - Debian y Ubuntu (Apache se llama
apache2): la carpeta/var/log/apache2/, conaccess.log,error.logyother_vhosts_access.log. Ojo con este último: usa un formato en el que el primer campo es el nombre del sitio y la IP pasa a ser el segundo, así que en las órdenes de abajo habría que cambiar$1por$2y sumar uno a los demás números. - Nginx:
/var/log/nginx/access.logyerror.log. Por defecto usa el mismo formato que Apache, así que las órdenes de esta guía sirven igual. - PHP-FPM: en Amazon Linux,
/var/log/php-fpm/(error.logpara los procesos ywww-error.logpara los errores de PHP); en Debian y Ubuntu,/var/log/php8.4-fpm.logo el de tu versión.
Si no los encuentras, pregúntale a la configuración de Apache dónde escribe:
sudo grep -RiE '^\s*(CustomLog|ErrorLog)' /etc/httpd/ /etc/apache2/ 2>/dev/null
Una ruta como logs/access_log es relativa a la carpeta de Apache: en Amazon Linux, /etc/httpd/logs apunta a /var/log/httpd.
En el servidor del laboratorio, la carpeta tiene esta pinta. Fíjate en el archivo que termina en -20261022: es un registro rotado. Para que no crezcan sin fin, el sistema los cierra cada cierto tiempo (cada semana en Amazon Linux, cada día en Debian y Ubuntu) y empieza uno nuevo, así que lo de hace unos días puede estar en el archivo anterior. En Debian y Ubuntu, además, los antiguos se guardan comprimidos (.gz).
sudo ls -la /var/log/httpd/
salidatotal 112 drwx------ 2 root root 4096 Oct 22 14:46 . drwxr-xr-x 1 root root 4096 Oct 22 14:45 .. -rw-r--r-- 1 root root 0 Oct 22 14:44 access_log -rw-r--r-- 1 root root 491 Oct 22 14:46 error_log -rw-r--r-- 1 root root 13045 Oct 22 14:47 mitienda.test-access.log -rw-r--r-- 1 root root 79696 Oct 22 14:46 mitienda.test-access.log-20261022 -rw-r--r-- 1 root root 0 Oct 22 14:46 mitienda.test-error.log -rw-r--r-- 1 root root 1728 Oct 22 14:46 mitienda.test-error.log-20261022
Paso 2: el tropiezo del asterisco con sudo
Lo normal es querer leer todos los registros de una vez con un asterisco, como *access*. Pero en Amazon Linux y otros sistemas, la carpeta de registros solo la puede abrir root, y pasa esto:
ls /var/log/httpd/ sudo grep -c POST /var/log/httpd/*access*
salidals: cannot open directory '/var/log/httpd/': Permission denied grep: /var/log/httpd/*access*: No such file or directory
¿Por qué falla si va con sudo? Porque el asterisco no lo interpreta grep, sino tu terminal, antes de ejecutar la orden, y tu usuario no puede ver qué archivos hay en esa carpeta. Como no encuentra ninguno, le pasa a grep el texto *access* tal cual, y grep busca un archivo con ese nombre. La solución es meter la orden dentro de sudo sh -c '…': así es root quien interpreta el asterisco.
sudo sh -c 'grep -c POST /var/log/httpd/*access*'
salida/var/log/httpd/access_log:0 /var/log/httpd/mitienda.test-access.log:122 /var/log/httpd/mitienda.test-access.log-20261022:767
Ese mismo truco va en todas las órdenes que siguen. Y si tus registros antiguos están comprimidos, cambia cat por zcat -f, que lee igual los comprimidos y los normales.
Paso 3: leer una línea del registro
Cada petición ocupa una línea. Esta es una del laboratorio, de la IP que intentaba adivinar la contraseña:
una línea del registro de acceso203.0.113.66 - - [22/Oct/2026:14:45:58 +0000] "POST /wp-login.php HTTP/1.1" 200 6034 "-" "Mozilla/5.0"
Para las órdenes de esta guía importan cuatro campos. Contando los trozos separados por espacios, la IP es el campo 1, el método (GET, POST) el 6, la ruta el 7 y el código de respuesta el 9. Por eso verás $1, $6, $7 y $9 en las órdenes de awk.
mod_remoteip en Apache), o tendrás que mirar los registros del propio proxy.Paso 4: quién te manda más peticiones POST
Las visitas normales son casi todas GET: pedir una página. Los ataques que importan, en cambio, suelen ser POST: enviar un formulario de inicio de sesión, una llamada a xmlrpc.php o un archivo. Así que lo primero es contar los POST por IP en el periodo que te interesa. Esta orden toma del 17 al 22 de octubre de 2026 (cambia el filtro por tus fechas):
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | grep -E '\[(1[7-9]|2[0-2])/Oct/2026' | awk '$6 ~ /POST/ {print $1}' | sort | uniq -c | sort -rn | head -20salida 421 203.0.113.66
151 203.0.113.77
150 203.0.113.79
150 203.0.113.78
14 203.0.113.5
2 203.0.113.25
1 203.0.113.10Ya se intuye la historia: una IP con cientos de POST, tres con exactamente 150 (un reparto así de redondo es típico de una red de bots que se divide el trabajo) y unas pocas con un puñado. La última, 203.0.113.10, es el propio servidor: lo vemos en el paso 6.
>? Es tentador filtrar con algo como $4 >= "[17/Oct/2026", pero awk compararía el texto letra por letra, y [20/Sep/2026 saldría «mayor» que [17/Oct/2026 porque el 2 va después del 1. Por eso el filtro usa grep con los días concretos.Paso 5: a dónde iban y qué recibieron
Ahora, para cada IP, a qué ruta mandaba los POST y qué código le devolvió el servidor:
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | grep -E '\[(1[7-9]|2[0-2])/Oct/2026' | awk '$6 ~ /POST/ {sub(/\?.*/, "", $7); print $1, $7, $9}' | sort | uniq -c | sort -rn | head -20salida 421 203.0.113.66 /wp-login.php 200
151 203.0.113.77 /xmlrpc.php 200
150 203.0.113.79 /xmlrpc.php 200
150 203.0.113.78 /xmlrpc.php 200
12 203.0.113.5 /wp-admin/admin-ajax.php 200
2 203.0.113.5 /wp-login.php 302
1 203.0.113.25 /alfa.php 404
1 203.0.113.25 / 200
1 203.0.113.10 /wp-cron.php 200Con esto ya se puede leer qué pasó. La clave es saber qué responde WordPress en cada caso, porque el código no siempre significa lo que parece:
POST /wp-login.phpcon 200: intento fallido. Cuando la contraseña es incorrecta, WordPress vuelve a mostrar el formulario con el mensaje de error, y eso es una respuesta 200 («todo bien»… para el servidor). Los 421 intentos de203.0.113.66son una fuerza bruta que no acertó ni una.POST /wp-login.phpcon 302: alguien entró. Cuando la contraseña es buena, WordPress redirige al panel, y eso es un 302. Es el código que hay que vigilar (paso 6).POST /xmlrpc.phpcon 200: tampoco es un acceso.xmlrpc.phpes una puerta antigua para publicar desde otras aplicaciones, y siempre responde 200, aunque la contraseña sea mala: el error va dentro de la respuesta. En el laboratorio, cada intento recibió un «Incorrect username or password».POST /wp-admin/admin-ajax.phpcon 200, desde tu IP: normal. Con el panel abierto, WordPress manda un «latido» cada 15 a 60 segundos para guardar borradores y avisar si otro está editando lo mismo.- 403: el servidor se negó (por ejemplo, porque bloqueaste esa ruta). 404: lo que pidió no existe, típico de los escáneres.
- 503 o 429: si tienes un límite de intentos (un plugin de seguridad, un módulo del servidor o un cortafuegos), así es como suele frenar a quien insiste. Que aparezcan es buena señal: la protección funciona.
Paso 6: ¿entró alguien?
Es la pregunta importante, y se responde buscando los 302 de wp-login.php:
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | awk '$6 ~ /POST/ && $7 ~ /wp-login\.php/ && $9 == 302 {print $1, $4}'salida203.0.113.5 [22/Oct/2026:14:45:57 203.0.113.5 [22/Oct/2026:14:46:29
Solo aparece 203.0.113.5, que en el laboratorio era la administradora entrando con su contraseña. En tu caso deberían salir solo tus IP (la de casa, la de la oficina, la del móvil). Para saber cuál es la tuya, abre checkip.amazonaws.com desde tu ordenador.
Si aparece una IP que no reconoces, ve directamente al apartado «Si encuentras algo grave».
wp-login.php, ni un acceso por xmlrpc.php con la contraseña correcta, que también da 200. Si usas alguna de las dos cosas, revisa además los usuarios y las contraseñas de aplicación en el panel (Usuarios → tu perfil).Paso 7: reconocer el tráfico normal
Antes de alarmarte por una IP con muchas peticiones, descarta las de casa:
- Tu propio servidor. WordPress ejecuta sus tareas programadas (publicar entradas programadas, buscar actualizaciones) llamándose a sí mismo a
wp-cron.php, con su IP pública y el navegador «WordPress». En un sitio con tráfico pueden ser cientos al día. Para saber la IP pública de tu servidor, ejecuta en élcurl -s https://checkip.amazonaws.com.
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | grep 'wp-cron.php' | head -2
salida203.0.113.10 - - [22/Oct/2026:14:47:27 +0000] "POST /wp-cron.php?doing_wp_cron=1792680446.9913299083709716796875 HTTP/1.1" 200 - "-" "WordPress/7.1.3;
- Tú, con el panel abierto: muchos
POSTaadmin-ajax.phpdesde tu IP. - Los buscadores: Google, Bing y compañía hacen muchos
GET, pero casi nuncaPOST. - Los servicios que conectaste: Jetpack, copias de seguridad o la app de WordPress pueden usar
xmlrpc.phpo la API desde sus propias IP.
Paso 8: los escáneres y el registro de errores
Además de probar contraseñas, los bots recorren miles de sitios buscando archivos olvidados o puertas traseras de otros ataques. Dejan un rastro de 404:
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | awk '$9 == 404 {print $1, $7}' | sort | uniq -c | sort -rn | headsalida 2 203.0.113.25 /alfa.php
1 203.0.113.25 /wp-xx.php
1 203.0.113.25 /wp-content/plugins/x/up.php
1 203.0.113.25 /shell.php
1 203.0.113.25 /.git/config
1 203.0.113.25 /.envBuscaba un .env (donde muchas aplicaciones guardan contraseñas), la configuración de Git y archivos con nombres típicos de puertas traseras (shell.php, alfa.php). Que respondan 404 es lo que quieres. Este otro resumen agrupa los mensajes del registro de errores:
sudo sh -c 'cat /var/log/httpd/mitienda.test-error.log*' | awk -F'] ' '{print $NF}' | cut -c1-90 | sort | uniq -c | sort -rn | head -15salida 1 AH01276: Cannot serve directory /var/www/mitienda.test/public_html/wp-includes/: No matchi
1 AH01276: Cannot serve directory /var/www/mitienda.test/public_html/wp-content/uploads/: No
1 AH01276: Cannot serve directory /var/www/mitienda.test/public_html/wp-admin/js/: No matchi
1 AH00036: access to /aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaLos mensajes más comunes, traducidos:
- AH01276, «Cannot serve directory»: alguien pidió el listado de una carpeta y Apache se negó porque los listados están desactivados. Es lo correcto.
- AH00036 o AH00127: rutas absurdas o demasiado largas. Alguien está probando si el servidor falla con entradas raras.
- AH01071, «Primary script unknown»: se pidió a PHP-FPM un archivo
.phpque no existe. Otro escáner buscando puertas traseras. - AH01630, «client denied by server configuration»: una regla tuya bloqueó la petición, como la de
xmlrpc.phpque verás más abajo.
Paso 9: caídas y errores de PHP
Muchos ataques contra PHP, como PHP2Shell, provocan que PHP se quede sin memoria o que uno de sus procesos se caiga, aunque el ataque no llegue a funcionar. Eso queda en los registros de PHP-FPM. En el laboratorio provocamos las dos cosas a propósito para ver cómo se ven:
sudo sh -c 'cat /var/log/php-fpm/*error*.log*' | grep -iE 'SIGSEGV|SIGBUS|exited on signal|Allowed memory size|segfault' | tail -30
salida[22-Oct-2026 14:45:23] WARNING: [pool www] child 580 exited on signal 11 (SIGSEGV - core dumped) after 81.270932 seconds from start [22-Oct-2026 14:45:12 UTC] PHP Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 1052672 bytes) in /var/www/mitienda.test/public_html/prueba-memoria.php on line 1
En un servidor sano, esta orden no debería mostrar nada, o casi nada. Un error de memoria suelto puede ser un plugin mal hecho, pero muchas caídas (exited on signal 11) o errores de memoria en fechas en las que no tocaste nada merecen una investigación. Antes de fiarte de un resultado vacío, comprueba con sudo ls -la /var/log/php-fpm/ que los archivos existen y tienen contenido.
Lo que los registros no ven
El registro de acceso guarda la línea de la petición, pero no lo que se envió dentro. En el laboratorio mandamos una subida de archivo con el campo que usan los ataques de PHP2Shell, PHP_SESSION_UPLOAD_PROGRESS, y esto es lo que se puede encontrar:
sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | grep -c PHP_SESSION_UPLOAD_PROGRESS sudo sh -c 'cat /var/log/httpd/mitienda.test-access.log-* /var/log/httpd/mitienda.test-access.log' | grep '^203.0.113.25 .*"POST / '
salida0 203.0.113.25 - - [22/Oct/2026:14:46:17 +0000] "POST / HTTP/1.1" 200 69633 "-" "curl/8.21.0"
Cero coincidencias: la petición está, pero como un simple POST /. Para ver el contenido hace falta un cortafuegos de aplicaciones web (como AWS WAF, Cloudflare o ModSecurity) que lo inspeccione y lo registre. Por eso los registros sirven para ver patrones (quién insiste, a qué ruta, con qué resultado), y la defensa de verdad sigue siendo actualizar.
Si encuentras algo grave
Si ves un inicio de sesión que no es tuyo, caídas de PHP que no te explicas o archivos que no deberían estar, actúa en este orden:
- Cambia las contraseñas de todos los administradores y borra las contraseñas de aplicación que no reconozcas. Cambia también la de la base de datos (en
wp-config.php) y las claves de seguridad de ese archivo, lo que cierra todas las sesiones abiertas. - Revisa la lista de usuarios en el panel: un atacante suele crear un administrador nuevo con un nombre discreto.
- Busca archivos PHP donde no deberían estar. En la carpeta de subidas de WordPress solo debería haber imágenes y documentos: un
.phpahí es casi siempre una puerta trasera. En el laboratorio dejamos uno a propósito:
sudo find /var/www -path '*/wp-content/uploads/*' -name '*.php'
salida/var/www/mitienda.test/public_html/wp-content/uploads/2026/10/cache.php
- Busca archivos PHP modificados recientemente, por ejemplo los que son más nuevos que tu
wp-config.php. Las actualizaciones de WordPress y de los plugins también cambian archivos, así que compara las fechas con las de tus actualizaciones:sudo find /var/www -name '*.php' -newer /ruta/a/wp-config.php. - Si encuentras una puerta trasera, no te limites a borrarla. Lo más seguro es restaurar una copia de seguridad anterior al ataque, reinstalar WordPress y los plugins desde sus fuentes oficiales y, después, actualizar todo.
Mantén PHP al día
De nada sirve revisar los registros si el agujero sigue abierto. En Amazon Linux 2023, el repositorio está fijado a una versión del sistema, y un dnf upgrade normal puede no ver los parches nuevos. En el laboratorio, la imagen traía PHP 8.3.33 y solo al pedir la última versión del repositorio apareció la corregida:
rpm -qa | grep php8.3-fpm dnf check-update --releasever=latest 'php8.3-fpm'
salidaphp8.3-fpm-8.3.33-1.amzn2023.0.1.x86_64 php8.3-fpm.x86_64 8.3.36-1.amzn2023.0.1 amazonlinux
sudo dnf upgrade --refresh --releasever=latest 'php8.3*' sudo systemctl restart php-fpm systemctl show php-fpm -p ActiveEnterTimestamp
El último paso importa: los procesos de PHP-FPM que ya están en marcha siguen usando la versión vieja hasta que se reinician. La hora que muestra ActiveEnterTimestamp tiene que ser posterior a la actualización. En Debian y Ubuntu, el equivalente es sudo apt update && sudo apt upgrade y reiniciar el servicio php8.4-fpm (o el de tu versión).
Cierra las puertas que no usas
La fuerza bruta contra wp-login.php y xmlrpc.php es el ruido de fondo de cualquier WordPress. La documentación oficial de WordPress recomienda desactivar xmlrpc.php si no lo usas y limitar los intentos de inicio de sesión, mejor en el servidor o en un cortafuegos que con un plugin, porque un plugin gasta PHP en cada intento.
- Bloquear
xmlrpc.phpcon Apache 2.4: añade esto al final del.htaccessde tu sitio, fuera de las líneas de WordPress. Antes, comprueba que no lo necesitas: lo usan Jetpack y algunas aplicaciones antiguas para publicar desde el móvil.
<Files "xmlrpc.php">
Require all denied
</Files>en el laboratorio, después de añadirloxmlrpc tras el bloqueo: 403 portada: 200
- Limitar los intentos de inicio de sesión con un plugin de seguridad, con el cortafuegos de tu proveedor o, en Nginx, con
limit_req. - Usar contraseñas largas y únicas y, si puedes, la verificación en dos pasos para los administradores. Con eso, la fuerza bruta se queda en ruido.
- No bloquear IP una a una: las redes de bots cambian de dirección constantemente, así que suele servir de poco. Si quieres hacerlo, mejor con herramientas que lo automatizan, como fail2ban.
Resumen: las órdenes en un solo bloque
Para Amazon Linux y Apache, con registros con el nombre del sitio. Cambia las rutas y las fechas por las tuyas y, en Debian y Ubuntu, /var/log/httpd por /var/log/apache2. zcat -f lee igual los registros comprimidos y los normales.
# 1. Dónde están los registros
sudo ls -la /var/log/httpd/ /var/log/php-fpm/
# 2. Quién manda más POST (todos los registros de acceso, también los rotados)
sudo sh -c 'zcat -f /var/log/httpd/*access*' | awk '$6 ~ /POST/ {print $1}' | sort | uniq -c | sort -rn | head -20
# 3. A qué rutas y con qué respuesta
sudo sh -c 'zcat -f /var/log/httpd/*access*' | awk '$6 ~ /POST/ {sub(/\?.*/, "", $7); print $1, $7, $9}' | sort | uniq -c | sort -rn | head -20
# 4. Inicios de sesión correctos: solo deberían salir tus IP
sudo sh -c 'zcat -f /var/log/httpd/*access*' | awk '$6 ~ /POST/ && $7 ~ /wp-login\.php/ && $9 == 302 {print $1}' | sort | uniq -c
# 5. Caídas y errores de memoria de PHP
sudo sh -c 'zcat -f /var/log/php-fpm/*error*.log*' | grep -iE 'SIGSEGV|exited on signal|Allowed memory size' | tail -30
# 6. Archivos PHP en la carpeta de subidas (no debería haber ninguno)
sudo find /var/www -path '*/wp-content/uploads/*' -name '*.php'
# 7. Tu IP pública (en el servidor) y la versión de PHP
curl -s https://checkip.amazonaws.com
rpm -qa | grep phpSi te interesa el porqué de todo esto, puedes seguir con la guía de cómo funciona HTTPS por dentro o la de SSH. Y si tu servidor usa PHP, lee qué pasó con PHP2Shell: es el mejor ejemplo de por qué conviene saber leer los registros.
Fuentes: formato de los registros de Apache, en su documentación oficial; recomendaciones contra la fuerza bruta y sobre xmlrpc.php, en la documentación de WordPress; actualizaciones de Amazon Linux 2023 con --releasever, en la guía de AWS; señales de PHP2Shell (PHP_SESSION_UPLOAD_PROGRESS, investigar desde el 17 de octubre), en el blog de seguridad de AWS; direcciones de documentación, en el RFC 5737. Las salidas se obtuvieron en un laboratorio con Amazon Linux 2023, Apache 2.4.68, PHP 8.3 y WordPress 7.1.3; las rutas de Debian se comprobaron en Debian 13.
