secaudit.eu
Incidentes

Web shells en WordPress: encontrar y bloquear en uploads

Encuentre web shells en wp-content/uploads con comandos seguros, bloquee la ejecución de PHP en Apache y nginx, revise los permisos y cierre la vía de entrada.

Por , AngelForce OÜ 10 min de lectura

También en: English Deutsch Français Italiano Polski Português Türkçe

Si sospecha que hay una web shell de WordPress en uploads, tiene tres tareas distintas. Localizar el archivo y conservarlo. Impedir que las peticiones directas ejecuten PHP en wp-content/uploads. Cerrar la vía por la que entró el archivo. Cada tarea resuelve un problema diferente, y ninguna por sí sola equivale a una recuperación completa.

Este artículo ofrece los comandos y los ajustes de servidor para cada una de ellas y explica qué no consigue cada una.

Qué es una web shell y por qué wp-content/uploads es su sitio favorito

Una web shell es un script pequeño, normalmente en PHP, que da al atacante un panel de control remoto en su servidor. Según la configuración, le permite ejecutar comandos, leer wp-config.php con la contraseña de su base de datos, consultar la base de datos y subir más archivos.

A los atacantes les gusta wp-content/uploads porque WordPress necesita escribir ahí de forma habitual. En esa carpeta acaban los archivos multimedia, los adjuntos de formularios y las exportaciones de plugins, y sus archivos se sirven directamente por URL. Si un archivo PHP aterriza allí y el servidor lo ejecuta, cualquiera en internet puede invocarlo.

Diagrama: un formulario de subida vulnerable lleva a una shell en uploads. Debajo, tres medidas distintas: cerrar la vía de entrada, encontrar, conservar y eliminar el archivo, y denegar las peticiones coincidentes.
Cada paso resuelve una tarea distinta. Juntos forman parte de la limpieza, no de la recuperación completa.

Cómo llegan las web shells a wp-content/uploads

Las vías de entrada habituales son poco vistosas:

  • Un formulario de subida que acepta cualquier tipo de archivo, como un plugin de formulario de contacto o de candidaturas que guarda los adjuntos en uploads.
  • Un fallo de subida arbitraria de archivos en un plugin o tema.
  • Un control de acceso defectuoso, en el que cualquiera puede invocar una función pensada para administradores. El informe Patchstack State of WordPress Security in 2026, elaborado por un proveedor, lo señala como la categoría más explotada.
  • Una cuenta robada combinada con una debilidad. Las comprobaciones de tipo de archivo que WordPress aplica por defecto suelen impedir que un Editor suba un archivo .php. Una cuenta robada se convierte en vía de subida cuando ese rol también puede llegar a un fallo de subida de un plugin, o cuando la configuración del sitio concede la capacidad unfiltered_upload. Un administrador de un sitio individual normalmente puede instalar plugins y usar los editores de archivos del escritorio. DISALLOW_FILE_EDIT desactiva esos editores; DISALLOW_FILE_MODS bloquea además la instalación y la actualización de plugins y temas desde el escritorio. En multisitio, solo los superadministradores tienen esos poderes.

Las shells rara vez se llaman shell.php. Busque dobles extensiones como invoice.pdf.php, las extensiones alternativas .phtml y .phar, y nombres que imitan archivos de caché o de miniaturas. Código PHP escondido dentro de un .jpg es una señal de alarma, pero no se ejecuta solo por contener PHP. Solo se ejecuta si el servidor entrega ese archivo a PHP o si otro script vulnerable lo incluye.

El riesgo se concentra en los plugins. Patchstack atribuye a los plugins el 91 % de las vulnerabilidades nuevas del ecosistema WordPress en 2025. Para las vulnerabilidades muy explotadas, indica una mediana ponderada de 5 horas hasta la explotación masiva. Nuestra guía sobre plugins vulnerables explica cómo mantener ese ritmo.

Cómo encontrar una web shell en WordPress: archivos PHP en uploads

Estos comandos no modifican ningún archivo. Ejecútelos por SSH desde la raíz de WordPress, es decir, la carpeta que contiene wp-config.php. Empiece por las extensiones evidentes:

find wp-content/uploads -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) -ls

invoice.pdf.php coincide porque termina en .php. Para detectar variantes como .php5 o image.php.jpg, amplíe el patrón a -iname '*.ph*' y cuente con más ruido.

A continuación, liste los archivos modificados recientemente, o los más nuevos que una subida que usted sepa que es limpia:

find wp-content/uploads -type f -mtime -7 -ls
find wp-content/uploads -type f -newer wp-content/uploads/2026/09/known-good.jpg -ls

Los atacantes pueden falsificar las fechas de modificación, así que trátelas como indicios. Para encontrar código PHP en archivos con nombre de imagen:

grep -rli --include='*.jpg' --include='*.jpeg' --include='*.png' --include='*.gif' --include='*.webp' --include='*.ico' '<?php' wp-content/uploads

Tenga en cuenta que grep lee el contenido de los archivos, y leer un archivo puede actualizar su fecha de acceso. Si ya sospecha de una intrusión, pida antes una instantánea a su proveedor de hosting.

No todo resultado es malicioso. Algunos plugins dejan un index.php de relleno que solo contiene un comentario, y algunos plugins de caché o de formularios escriben PHP en su propia subcarpeta. Antes de abrir un candidato, guarde sus metadatos como se describe en la sección siguiente. Después, inspecciónelo con less o xxd file | head -40. No lo abra nunca en el navegador, porque podría ejecutarse. Cadenas codificadas muy largas, o eval, base64_decode o assert combinados con variables de la petición, son señales de alarma claras.

Busque también en el registro de accesos las peticiones a archivos PHP dentro de uploads. Un POST a un archivo así rara vez es legítimo:

grep -E '"(GET|POST) /wp-content/uploads/[^ ]*\.(php|phtml|phar)' access.log

¿Ha encontrado una? Conserve las pruebas antes de borrar nada

Primero registre los metadatos y después copie el archivo y los registros. Guárdelo todo en un directorio privado situado fuera de cualquier raíz de documentos que sirva su servidor. En algunos hostings compartidos el propio directorio personal es accesible por web, así que compruebe la raíz de documentos en su panel de control o en la configuración del vhost.

umask 077
mkdir -p ~/ir-evidence && chmod 700 ~/ir-evidence
f="wp-content/uploads/path/to/suspicious.php"
case_id="webshell-$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM"
stat "$f" > ~/ir-evidence/"$case_id"-stat.txt
sha256sum "$f" > ~/ir-evidence/"$case_id"-sha256.txt
tar -czpf ~/ir-evidence/"$case_id"-file.tar.gz "$f"
tar -czpf ~/ir-evidence/"$case_id"-logs.tar.gz /path/to/access.log* /path/to/error.log*

La marca de tiempo y el sufijo aleatorio reducen la probabilidad de reutilizar un nombre de archivo de pruebas; compruebe si ya existen nombres iguales antes de repetir la captura. Los proveedores rotan los registros, así que cópielos ahora. Después, descargue las pruebas a un lugar seguro fuera del servidor y anote la ruta y las líneas de registro correspondientes.

Trate como potencialmente expuestas las credenciales de la base de datos de wp-config.php. Valore si se ha accedido a datos personales, o si estos han sido divulgados, alterados, perdidos o hechos inaccesibles: una shell operativa es una señal de alarma seria, pero por sí sola no demuestra que haya una violación de la seguridad de los datos personales con arreglo al RGPD. El art. 33 exige notificarla a la autoridad de control (en España, la AEPD) sin dilación indebida y, de ser posible, a más tardar 72 horas después de haber tenido constancia de ella, salvo que sea improbable que constituya un riesgo para los derechos y las libertades de las personas. El art. 33, apartado 5, le obliga a documentar toda violación, también las que no notifique. Nuestra guía de las 72 horas explica los detalles. Para la limpieza completa, siga nuestro plan de eliminación de malware.

Bloquear la ejecución de PHP en uploads (Apache y nginx)

Los archivos multimedia legítimos nunca necesitan ejecutarse como PHP. En Apache 2.4, cree wp-content/uploads/.htaccess:

<FilesMatch "\.(?i:php|phtml|phar)$">
    Require all denied
</FilesMatch>

Apache solo respeta este archivo si AllowOverride lo permite para ese directorio, lo que en el caso de Require significa All o, como mínimo, AuthConfig. Si los overrides están desactivados, pida a su proveedor que coloque el bloque dentro de una sección <Directory> limitada a la ruta absoluta del directorio uploads de este sitio. No lo ponga sin acotar en el virtual host, porque podría bloquear PHP en todo el sitio.

En nginx, añada esto al bloque server:

location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ { deny all; }

nginx primero busca el location de prefijo más largo que coincida. Si ese location usa ^~, por ejemplo location ^~ /wp-content/uploads/, nginx nunca evalúa los locations con expresión regular y su regla se ignora. En ese caso, la regla de denegación debe ir dentro de ese bloque. En caso contrario, los locations con expresión regular se comprueban por orden y gana la primera coincidencia, de modo que esta regla debe ir antes de su bloque general location ~ \.php$. Si WordPress está en un subdirectorio como /blog/, o usa una URL de uploads personalizada, ajuste la ruta. Compruebe la configuración efectiva y recargue:

sudo nginx -T | grep -n 'location'
sudo nginx -t && sudo systemctl reload nginx

En un hosting gestionado, pida al soporte que aplique la regla.

Estas reglas deniegan las peticiones web directas que coinciden con las extensiones indicadas, siempre que la configuración del servidor las aplique. No cubren todos los sufijos ejecutables ni impiden que otro script PHP incluya un archivo subido. Tampoco eliminan shells ni evitan subidas. Pregunte a su proveedor qué sufijos se asocian a PHP y amplíe la lista si hace falta.

Compruebe la regla con un archivo de prueba

Use un nombre de archivo nuevo y aleatorio. Con noclobber activado, el comando se niega a sobrescribir un archivo existente y solo borra un archivo que haya creado él mismo:

t="wp-content/uploads/exec-test-$(date +%s)-$RANDOM.php"
if ( set -o noclobber; printf '%s\n' '<?php echo "marker-" . (40 + 2);' > "$t" ); then
  curl -sS -i "https://example.com/$t"
  rm -- "$t"
else
  echo "Not created: $t already exists"
fi

Si el cuerpo de la respuesta contiene marker-42, PHP ejecutó el archivo, porque esa cadena no aparece en el propio archivo. Si ve la línea <?php en bruto con estado 200, PHP no se ejecutó, pero el servidor está exponiendo código fuente. Es otro problema, que corresponde a su proveedor. Un estado 200 por sí solo no demuestra nada. Un 403 solo cuenta si lo ha producido su regla, y no una CDN o un WAF. Pruebe contra el servidor de origen con curl --resolve example.com:443:ORIGIN_IP y busque «access forbidden by rule» (nginx) o «client denied by server configuration» (Apache) en el registro de errores del origen.

Permisos y propietarios en WordPress: revisar, no restablecer

Los permisos y los propietarios deciden juntos qué puede modificar el proceso PHP. El propietario por sí solo no basta, porque los bits de grupo, los bits de «otros» y las ACL también conceden escritura. No ejecute un chmod recursivo sobre todo el sitio. Puede dejar archivos privados, como las copias de seguridad, legibles por cualquiera, y puede quitar el bit de ejecución a scripts de mantenimiento.

Primero averigüe quién es el propietario de los archivos y con qué usuario se ejecuta PHP:

stat -c '%U:%G %a %n' wp-config.php wp-content wp-content/uploads
ps -eo user,group,comm | grep -E 'php-fpm|php|httpd|apache2' | sort -u

En muchos hostings compartidos, PHP se ejecuta como el usuario de su propia cuenta. En ese escenario, los bits de solo lectura ordinarios ofrecen una protección limitada: el propietario del archivo puede, por lo general, cambiarlos para recuperar la escritura, salvo que otras restricciones del sistema de archivos o del hosting lo impidan.

Después, liste lo que el usuario de PHP puede escribir realmente, además de todo lo que sea escribible por cualquiera. Sustituya PHPUSER por el usuario que haya encontrado:

sudo -u PHPUSER find . -writable ! -type l -print
find . -perm -o+w ! -type l -ls

Revise las dos listas por separado: las rutas escribibles por cualquiera suelen requerir corrección, mientras que las escribibles por PHP dependen de cómo actualice. Un despliegue de solo lectura debería limitar normalmente las escrituras de PHP a uploads y a las carpetas de ejecución necesarias; las actualizaciones desde el escritorio y las automáticas requieren permisos de escritura adicionales. Corrija una a una las rutas revisadas con notación simbólica, por ejemplo chmod g-w,o-w path/to/file. Quitar bits nunca relaja un modo existente más estricto. La guía de refuerzo de WordPress considera 755 para directorios y 644 para archivos como el límite superior habitual. Ponga wp-config.php en 640 o 600, según cómo lo lea PHP, y pruebe después el sitio. No use nunca 777.

Hay un límite que no se puede evitar. PHP tiene que poder escribir en uploads, de modo que un plugin vulnerable que se ejecute con ese usuario podrá seguir escribiendo allí.

define( 'DISALLOW_FILE_EDIT', true ); en wp-config.php elimina los editores de archivos del escritorio. DISALLOW_FILE_MODS va más lejos, pero también desactiva las actualizaciones automáticas en segundo plano. Úselo solo si actualiza por otra vía.

Cierre la brecha por la que entró la shell y siga vigilando uploads

Borrar la shell no corrige la causa. Mientras la vía de entrada siga abierta, la reinfección es posible.

Compare la salida de stat que guardó con el registro de accesos. Busque el formulario o el endpoint del plugin que recibió un POST poco antes de que apareciera el archivo. Después, actualice el plugin o reconfigúrelo: restrinja los tipos de archivo permitidos y, si el plugin lo admite, guarde los adjuntos fuera de la raíz web. Si no hay una actualización ni una mitigación seguras, elimine el plugin. Desactivarlo no es un sustituto fiable, porque sus archivos siguen en disco y algunos pueden seguir siendo accesibles directamente. Después, cambie las contraseñas de administrador, las salts de wp-config.php y las credenciales de la base de datos.

Siga vigilando a partir de ahí:

  • Ejecute el comando find combinado en una tarea cron diaria que le envíe por correo cualquier salida.
  • Añada monitorización de integridad de archivos.
  • Ejecute wp core verify-checksums con regularidad.

El plugin Security Audit & Compliance de secaudit.eu ayuda con la monitorización. Mantiene una línea base de integridad de archivos, informa de los cambios y del contenido ofuscado, y comprueba firmas de malware e indicadores de compromiso. Detecta e informa. No elimina nada. Para otras señales de alarma, consulte 12 señales de que su sitio WordPress ha sido hackeado.

FAQ

¿Es malicioso todo archivo PHP en wp-content/uploads?

No. Los index.php de relleno y algunas carpetas de plugins son legítimos. Registre los metadatos, inspeccione el archivo sin ejecutarlo y compruebe a qué plugin pertenece la carpeta.

¿Un plugin de seguridad o un WAF hace innecesaria la regla de uploads?

No. Un firewall puede no reconocer un patrón de ataque nuevo. La regla del servidor es una capa independiente que deniega las peticiones directas a las extensiones indicadas aunque se cuele una subida.

¿Basta con bloquear PHP en uploads si ya he encontrado una shell?

No. Conserve las pruebas, limpie el sitio y cierre la vía de entrada. Puede que la shell ya se haya usado para plantar archivos en otros lugares o para leer su base de datos.

Este artículo es información general y no constituye asesoramiento jurídico.

Compruebe su propio sitio

Security Audit & Compliance es el plugin de WordPress de secaudit.eu: busca malware, spam oculto, archivos del núcleo modificados y código vulnerable, y relaciona sus hallazgos con el art. 32 del RGPD, el art. 21 de NIS2 y el OWASP Top 10.

Ver qué comprueba el plugin
Melike Görel

Melike Görel escribe las guías de secaudit.eu sobre seguridad de WordPress y cumplimiento normativo europeo para AngelForce OÜ, la empresa de software de Tallin que está detrás de secaudit.eu.

Elaborado con ayuda de IA y verificado antes de su publicación. Este artículo es información general, no asesoramiento jurídico.