WordPress Hackeado: Cómo Detectar y Eliminar una Webshell

Guía práctica basada en un caso real: desde los primeros indicios hasta la limpieza completa del servidor y las medidas para que no vuelva a ocurrir.

Un día tu web funciona con normalidad. Al día siguiente empieza a dar errores aleatorios, aparecen páginas que no has creado o recibes alertas del antivirus de tus lectores. En el peor de los casos no notas nada: el atacante lleva semanas dentro y ha instalado una webshell, un archivo PHP oculto que le permite ejecutar comandos en tu servidor como si tuviera acceso directo.

Este artículo documenta paso a paso cómo detectar, analizar y neutralizar este tipo de ataque, basado en un caso real de un sitio WordPress con varios plugins activos. Si gestionas una web para tu negocio en Barbastro, Huesca o cualquier otro punto de Aragón, este es el mismo proceso que sigo cuando un cliente me llega con un WordPress comprometido.

⚠ Antes de empezar

Si sospechas que tu web está comprometida en este momento, no la apagues ni restaures todavía. Primero recopila evidencias: logs de acceso, lista de archivos modificados y volcado de la base de datos. Una restauración precipitada borra las pruebas y no garantiza que el vector de entrada esté cerrado.

Qué es una webshell y cómo llega

Una webshell es un archivo PHP (o a veces JSP, ASPX) que el atacante sube al servidor y que le permite ejecutar comandos del sistema operativo a través del navegador. Desde ahí puede leer archivos de configuración, crear usuarios, enviar spam, instalar malware o usar tu servidor como trampolín para atacar otros sitios.

Vectores de entrada más habituales

Indicadores de compromiso (IoC)

Antes de buscar archivos, revisa estas señales de alerta en tu servidor:

IndicadorQué significaDónde buscarlo
/wp-content/uploads/*.phpPHP en la carpeta de subidas — WordPress nunca pone PHPs aquíFTP / SSH / panel de archivos
eval(base64_decode(...))Código ofuscado en base64, firma clásica de malwaregrep en el servidor
Usuarios admin desconocidosEl atacante se crea una cuenta de administrador para persistirWordPress → Usuarios o BD
Peticiones POST a archivos de imagenUna imagen no debería recibir POST — es la webshell respondiendoaccess_log del servidor
IPs extranjeras con muchas 200 a /uploads/El atacante usando su webshell repetidamenteaccess_log
PHPs con fecha de modificación reciente en lugares rarosModificación post-instalación sospechosafind con -newer
Opciones de WordPress modificadas (siteurl, admin_email)Redirección o toma de control del panelwp_options en la BD

Proceso de detección paso a paso

  1. Buscar PHPs en carpetas que no deberían tenerlos

    La carpeta wp-content/uploads/ solo debería contener imágenes, PDFs y documentos. Cualquier .php ahí es malicioso por definición.

    Buscar PHPs sospechosos
    # Buscar PHPs en uploads find /var/www/html/wp-content/uploads/ -name "*.php" -type f # Buscar también extensiones disfrazadas find /var/www/html/wp-content/uploads/ \ -name "*.php*" -o -name "*.phtml" -o -name "*.php5" # PHPs modificados en los últimos 30 días en todo el sitio find /var/www/html/ -name "*.php" -newer /var/www/html/wp-config.php \ -not -path "*/node_modules/*" | sort
  2. Buscar código ofuscado o funciones peligrosas

    Las webshells casi siempre usan eval, base64_decode, system o passthru para ejecutar comandos. Un grep rápido delata la mayoría:

    Grep de patrones de webshell
    # Código ofuscado en base64 grep -r "eval(base64_decode" /var/www/html/ --include="*.php" -l # Funciones de ejecución de comandos en uploads grep -rE "(system|exec|passthru|shell_exec|popen)\s*\(" \ /var/www/html/wp-content/uploads/ --include="*.php" # Patrón típico de webshell con variable mezclada grep -r '$_POST\[.*\].*eval' /var/www/html/ --include="*.php" -l # Buscar base64 sospechoso (cadenas largas > 500 chars) grep -rP "[A-Za-z0-9+/]{500,}" /var/www/html/wp-content/ \ --include="*.php" -l 2>/dev/null
  3. Revisar los logs de acceso

    El access_log del servidor es la fuente más fiable. Busca peticiones a rutas que no son URLs normales de WordPress, especialmente con códigos HTTP 200 (éxito):

    Análisis del access_log
    # Peticiones POST exitosas a /uploads/ (sospechoso) grep "POST" /var/log/apache2/access.log | grep "/uploads/" | grep " 200 " # IPs con más de 50 peticiones a PHP en uploads grep "/uploads/.*\.php" /var/log/apache2/access.log | \ awk '{print $1}' | sort | uniq -c | sort -rn | head -20 # Accesos a archivos PHP desconocidos con 200 grep " 200 " /var/log/apache2/access.log | \ grep "\.php" | grep -v "wp-" | grep -v "index" | \ awk '{print $7}' | sort | uniq -c | sort -rn | head -20
  4. Revisar usuarios administradores en la base de datos

    Los atacantes suelen crearse un usuario administrador para mantener acceso incluso si se borra la webshell. Comprueba directamente en la BD:

    Usuarios con rol administrator
    -- En MySQL/MariaDB SELECT u.ID, u.user_login, u.user_email, u.user_registered, um.meta_value as rol FROM wp_users u JOIN wp_usermeta um ON u.ID = um.user_id WHERE um.meta_key = 'wp_capabilities' AND um.meta_value LIKE '%administrator%' ORDER BY u.user_registered DESC;

    Cualquier administrador que no reconozcas, especialmente con fecha de registro reciente o email en dominio desconocido, es un backdoor.

  5. Comprobar opciones críticas de WordPress

    Algunos ataques modifican la URL del sitio o el email de administrador para redirigir tráfico o tomar control del panel de recuperación de contraseña:

    Opciones sensibles en wp_options
    SELECT option_name, option_value FROM wp_options WHERE option_name IN ( 'siteurl', 'home', 'admin_email', 'blogname', 'users_can_register', 'default_role' );

Neutralización: limpieza paso a paso

🔒 Antes de limpiar

Haz un volcado completo de la BD (mysqldump) y una copia del directorio web. Si el proveedor tiene snapshots, toma uno. Trabajarás sobre el sistema en vivo y necesitas poder revertir si algo sale mal.

Bloquear el acceso al atacante

Lo primero es cortar el acceso activo. Si tienes las IPs del atacante en los logs, bloquéalas en el firewall antes de tocar nada:

# Con iptables iptables -A INPUT -s 185.220.101.0/24 -j DROP iptables -A INPUT -s 185.220.101.1 -j DROP # Guardar para que persista tras reinicio (Debian/Ubuntu) iptables-save > /etc/iptables/rules.v4

Si usas Cloudflare, bloquea las IPs desde el panel de Firewall Rules. Es más rápido y no requiere acceso SSH.

Eliminar los archivos maliciosos

# Ver el contenido antes de borrar — nunca elimines a ciegas cat /var/www/html/wp-content/uploads/2026/01/image.php # Eliminar webshells encontradas rm /var/www/html/wp-content/uploads/2026/01/image.php rm /var/www/html/wp-content/uploads/cache/tmp.php # Si hay muchas, eliminar todas las PHP de uploads de una vez # (verificar primero con find sin -delete) find /var/www/html/wp-content/uploads/ -name "*.php" -delete

Eliminar usuarios administradores creados por el atacante

-- Borrar el usuario y sus metadatos DELETE um FROM wp_usermeta um JOIN wp_users u ON um.user_id = u.ID WHERE u.ID = 999; -- ID del usuario malicioso DELETE FROM wp_users WHERE ID = 999;

Cambiar todas las credenciales

Regenerar las claves de seguridad en wp-config.php

// Sustituir estas líneas en wp-config.php por las nuevas generadas define('AUTH_KEY', 'nueva-clave-aleatoria-aquí'); define('SECURE_AUTH_KEY', 'nueva-clave-aleatoria-aquí'); define('LOGGED_IN_KEY', 'nueva-clave-aleatoria-aquí'); define('NONCE_KEY', 'nueva-clave-aleatoria-aquí'); define('AUTH_SALT', 'nueva-clave-aleatoria-aquí'); define('SECURE_AUTH_SALT', 'nueva-clave-aleatoria-aquí'); define('LOGGED_IN_SALT', 'nueva-clave-aleatoria-aquí'); define('NONCE_SALT', 'nueva-clave-aleatoria-aquí');

Verificar la integridad de WordPress

Compara los archivos del core con la versión oficial para detectar modificaciones:

# Con WP-CLI — compara checksums con el repositorio oficial wp core verify-checksums # Si hay diferencias, reinstala el core sin tocar wp-config ni wp-content wp core download --skip-content --force

Desactivar temporalmente el registro de usuarios

Hasta que confirmes que el vector de entrada está cerrado, desactiva el registro para evitar que el atacante cree nuevas cuentas:

UPDATE wp_options SET option_value = '0' WHERE option_name = 'users_can_register';
Señal de que la limpieza fue efectiva

Después de limpiar, monitoriza el access_log durante 24-48 horas. Si el atacante sigue intentando acceder a las rutas donde estaban sus webshells y recibe 404, el vector está cerrado. Si recibe 200, hay otro archivo que no encontraste.

Medidas preventivas para que no vuelva a ocurrir

Proteger la carpeta uploads contra ejecución de PHP

Esta es la medida más efectiva. Aunque el atacante suba un PHP, el servidor no lo ejecutará:

# Añadir en wp-content/uploads/.htaccess <Files *.php> deny from all </Files> <FilesMatch "\.(php|php5|phtml|pl|py|jsp|asp|htm|shtml|sh|cgi)$"> deny from all </FilesMatch>

Limitar intentos de login

Instala un plugin como Limit Login Attempts Reloaded o configura fail2ban en el servidor para bloquear IPs tras varios intentos fallidos de acceso al panel. Si gestionas también el servidor, esto encaja con el mismo tipo de bastionado que explico en la guía de hardening de sistemas Windows y Linux.

Mantener todo actualizado

La mayoría de las webshells entran por vulnerabilidades conocidas en plugins desactualizados. Activa las actualizaciones automáticas para el core y revisa regularmente los plugins:

# Ver plugins con actualizaciones pendientes wp plugin list --update=available # Actualizar todo wp plugin update --all wp core update

Activar autenticación de dos factores

Para todos los usuarios con rol de administrador o editor. Plugins como WP 2FA o Google Authenticator añaden una capa adicional que hace inútiles las contraseñas robadas.

Usar contraseñas únicas y largas

Una contraseña de 20 caracteres aleatorios hace los ataques de fuerza bruta computacionalmente inviables. Usa un gestor de contraseñas (Bitwarden, 1Password) para generarlas y almacenarlas.

Monitorizar cambios en los archivos

Configura un sistema que te alerte cuando se modifiquen archivos PHP fuera de las actualizaciones planificadas. Wordfence tiene esta función, o puedes usar inotifywait en el servidor:

# Monitorizar cambios en tiempo real (Linux) inotifywait -m -r /var/www/html/wp-content/uploads/ \ -e create,modify --format '%T %w%f' --timefmt '%Y-%m-%d %H:%M:%S'

Configurar permisos correctos

# Permisos recomendados para instalación WordPress find /var/www/html/ -type d -exec chmod 755 {} \; find /var/www/html/ -type f -exec chmod 644 {} \; chmod 600 /var/www/html/wp-config.php # El propietario debe ser el usuario del servidor web, no root chown -R www-data:www-data /var/www/html/

Limpiar usuarios subscriber spam periódicamente

Los bots registran cuentas de subscriber automáticamente. No tienen permisos peligrosos, pero acumulan basura en la BD y pueden usarse en ataques posteriores:

-- Ver subscribers con emails de dominios spam conocidos SELECT ID, user_login, user_email, user_registered FROM wp_users u JOIN wp_usermeta um ON u.ID = um.user_id WHERE um.meta_key = 'wp_capabilities' AND um.meta_value LIKE '%subscriber%' AND (user_email LIKE '%backlinks%' OR user_email LIKE '%gsasearch%' OR user_email LIKE '%autommo%') ORDER BY user_registered DESC;

Resumen del flujo completo

FaseAcción principalHerramienta
DetecciónBuscar PHPs en uploads y código ofuscadofind, grep, SSH
AnálisisRevisar logs de acceso, IPs, usuarios BDaccess_log, MySQL
ContenciónBloquear IPs atacantes, desactivar registroiptables, wp_options
LimpiezaBorrar webshells, usuarios y opciones comprometidasrm, MySQL, WP-CLI
RecuperaciónCambiar todas las contraseñas y claves secretaswp-config.php
PrevenciónBloquear ejecución en uploads, 2FA, actualizaciones.htaccess, plugins
Punto clave

El paso más importante no es la limpieza sino encontrar y cerrar el vector de entrada. Si el atacante entró por un plugin vulnerable y ese plugin sigue instalado y sin actualizar, volverá a entrar en horas. La limpieza sin cerrar la puerta es trabajo perdido.

Checklist prioritario si tu WordPress está hackeado

Si tienes que empezar por algún sitio, este es el orden que más impacto tiene en el menor tiempo posible:

Preguntas Frecuentes sobre WordPress Hackeado

No apagues la web ni restaures un backup todavía. Primero recopila evidencias: copia los logs de acceso, haz una lista de los archivos PHP modificados recientemente y guarda un volcado de la base de datos. Restaurar antes de investigar borra las pruebas y no garantiza que el vector de entrada esté cerrado, así que el atacante podría volver a entrar por el mismo sitio.

Una webshell es un archivo PHP que el atacante sube al servidor y que le permite ejecutar comandos del sistema desde el navegador. Los vectores más habituales son plugins o temas con vulnerabilidades de subida de archivos, credenciales robadas o por fuerza bruta, formularios de contacto mal configurados y servidores compartidos donde otro dominio ya estaba comprometido.

Depende de la gravedad: cuántos archivos están afectados, si hay usuarios administradores creados por el atacante y si tu web ha entrado en listas negras de Google o del hosting. El diagnóstico inicial es gratuito; a partir de ahí se da un presupuesto cerrado según el alcance real de la limpieza.

Revisa Google Search Console, sección Seguridad y acciones manuales. Si aparece un aviso de contenido pirateado o malware, Google ya ha marcado tu web. También puedes comprobarlo directamente en el Transparency Report de Google Safe Browsing introduciendo tu dominio. Una vez limpio el sitio, se solicita una revisión desde Search Console para salir de la lista.

No, si no cierras antes el vector de entrada. Si el backup es anterior al ataque pero el plugin vulnerable sigue instalado y desactualizado, o las credenciales robadas siguen siendo válidas, el atacante volverá a entrar en horas. Restaurar sin investigar y cerrar la puerta de entrada es limpiar el suelo con la ventana rota abierta.

Las medidas de mayor impacto son: bloquear la ejecución de PHP en wp-content/uploads, mantener el core y los plugins siempre actualizados, activar autenticación de dos factores para todos los administradores, usar contraseñas largas y únicas, y monitorizar cambios en archivos PHP fuera de las actualizaciones planificadas.

¿Tu WordPress está hackeado ahora mismo?

Diagnóstico gratuito y reparación urgente de webs hackeadas o comprometidas en Barbastro, Huesca y el resto de Aragón. Elimino malware y backdoors, cierro el vector de entrada y dejo tu web funcionando y segura.

Solicitar reparación urgente →

← Volver al Blog

💬 5 comentarios

RP

Justo lo que necesitaba. Tenía un WooCommerce con peticiones POST raras a /uploads/ en el access_log y no sabía interpretarlo. El grep de eval(base64_decode encontró dos archivos que llevaban ahí semanas. Lo que no sabía es que también debía revisar wp_options por si habían cambiado el admin_email, menos mal que lo leí antes de dar la limpieza por terminada.

LJ

Raúl, ese es uno de los pasos que más se saltan. El admin_email es crítico porque si lo cambian pueden pedir "olvidé mi contraseña" y recuperar el acceso al panel aunque hayas cambiado todas las contraseñas. Revísalo siempre junto a siteurl y home antes de dar la limpieza por cerrada.

MS

Llevaba tres días con una web en la lista negra de Google Safe Browsing sin saber por qué caía el tráfico a cero. El aviso estaba en Search Console, en Seguridad y acciones manuales, tal como dices. Después de limpiar y pedir la revisión tardó unas 24 horas en quitarse el aviso. Un susto importante para un comercio pequeño.

JC

El .htaccess de uploads para bloquear PHP debería venir activado por defecto en WordPress, punto. Lo apliqué en las cuatro webs que gestiono para clientes y en ninguna rompió nada, porque nunca debería haber PHP ejecutable ahí. Cinco minutos de trabajo por una tranquilidad enorme.

LJ

Totalmente de acuerdo, Jorge. Es de las pocas medidas de seguridad con coste cero y beneficio altísimo. La única precaución es si tienes algún plugin que genere PDFs o imágenes dinámicamente dentro de uploads vía PHP —son casos raros, pero conviene comprobarlo antes de aplicar la regla en producción.

Se ha cerrado el hilo de comentarios para este artículo.