secaudit.eu
Incidents

Web shells WordPress dans uploads : les trouver et les bloquer

Repérez un web shell WordPress dans wp-content/uploads, bloquez l'exécution de PHP sur Apache et nginx, vérifiez les droits de fichiers et fermez la porte d'entrée.

Par , AngelForce OÜ 10 min de lecture

Aussi en : English Deutsch Español Italiano Polski Português Türkçe

Si vous soupçonnez un web shell WordPress dans uploads, il y a trois tâches distinctes. Vous trouvez le fichier et vous le conservez. Vous empêchez les requêtes directes d'exécuter du PHP dans wp-content/uploads. Vous fermez la voie par laquelle le fichier est entré. Chaque tâche règle un problème différent, et aucune ne suffit à elle seule pour remettre le site en état.

Cet article donne les commandes et les réglages serveur pour chacune d'elles, et précise ce que chacune ne fait pas.

Qu'est-ce qu'un web shell, et pourquoi wp-content/uploads est son terrain favori

Un web shell est un petit script, le plus souvent en PHP, qui offre à un attaquant un panneau de contrôle à distance sur votre serveur. Selon la configuration, il peut lui permettre d'exécuter des commandes, de lire wp-config.php avec le mot de passe de votre base de données, d'interroger la base et d'envoyer d'autres fichiers.

Les attaquants apprécient wp-content/uploads parce que WordPress a normalement besoin d'y écrire. Les médias, les pièces jointes de formulaires et les exports de extensions atterrissent tous dans ce dossier, dont les fichiers sont servis directement par URL. Si un fichier PHP s'y trouve et que le serveur l'exécute, n'importe qui sur internet peut l'appeler.

Schéma : un formulaire d'envoi vulnérable mène à un shell dans uploads. Trois actions distinctes en dessous : fermer la voie d'envoi, trouver, conserver et supprimer le fichier, et refuser les requêtes correspondantes.
Chaque étape a son rôle. Ensemble, elles font partie du nettoyage, pas de la remise en état complète.

Comment les web shells arrivent dans wp-content/uploads

Les voies d'entrée habituelles n'ont rien de spectaculaire :

  • Un formulaire d'envoi qui accepte n'importe quel type de fichier, par exemple une extension de formulaire de contact ou de candidature qui stocke les pièces jointes dans uploads.
  • Une faille d'envoi de fichier arbitraire dans une extension ou un thème.
  • Un contrôle d'accès défaillant : une fonction réservée aux administrateurs peut être appelée par n'importe qui. Le rapport Patchstack State of WordPress Security in 2026, un rapport d'éditeur, en fait la catégorie la plus exploitée.
  • Un compte volé combiné à une faiblesse. Les contrôles de type de fichier par défaut de WordPress empêchent normalement un Éditeur d'envoyer un fichier .php. Un compte volé devient une voie d'envoi si ce rôle peut aussi atteindre une faille d'envoi dans une extension, ou si la configuration du site accorde la capacité unfiltered_upload. Sur un site unique, un Administrateur peut en général installer des extensions et utiliser les éditeurs de fichiers du tableau de bord. DISALLOW_FILE_EDIT désactive ces éditeurs ; DISALLOW_FILE_MODS bloque en plus l'installation et la mise à jour des extensions et des thèmes depuis le tableau de bord. En multisite, seuls les super-administrateurs disposent de ces pouvoirs.

Un shell s'appelle rarement shell.php. Cherchez les doubles extensions comme invoice.pdf.php, les extensions alternatives .phtml et .phar, et les noms qui imitent des fichiers de cache ou de miniatures. Du code PHP caché dans un .jpg est un signal d'alerte, mais il ne s'exécute pas du seul fait qu'il contient du PHP. Il ne s'exécute que si le serveur confie ce fichier à PHP, ou si un autre script vulnérable l'inclut.

Le risque se concentre dans les extensions. Patchstack attribue aux extensions 91 % des nouvelles vulnérabilités de l'écosystème WordPress en 2025. Pour les vulnérabilités très exploitées, il indique une médiane pondérée de 5 heures avant l'exploitation de masse. Notre guide sur les extensions vulnérables explique comment suivre ce rythme.

Trouver un web shell WordPress : fichiers PHP dans uploads

Ces commandes ne modifient aucun fichier. Lancez-les en SSH depuis la racine de WordPress, c'est-à-dire le dossier qui contient wp-config.php. Commencez par les extensions les plus évidentes :

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

invoice.pdf.php correspond puisqu'il se termine par .php. Pour attraper des variantes comme .php5 ou image.php.jpg, élargissez le motif à -iname '*.ph*' et attendez-vous à plus de bruit.

Listez ensuite les fichiers modifiés récemment, ou plus récents qu'un fichier envoyé dont vous savez qu'il est sain :

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

Les attaquants peuvent falsifier les dates de modification : prenez-les comme des indices. Pour trouver du code PHP dans des fichiers qui portent un nom d'image :

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

Notez que grep lit le contenu des fichiers, et que cette lecture peut mettre à jour la date d'accès. Si vous soupçonnez déjà une compromission, demandez d'abord un instantané (snapshot) à votre hébergeur.

Tout résultat n'est pas malveillant. Certaines extensions déposent un index.php factice qui ne contient qu'un commentaire, et certaines extensions de cache ou de formulaires écrivent du PHP dans leur propre sous-dossier. Avant d'ouvrir un fichier suspect, enregistrez ses métadonnées comme décrit dans la section suivante. Inspectez-le ensuite avec less ou xxd file | head -40. Ne l'ouvrez jamais dans un navigateur : cela pourrait l'exécuter. De longues chaînes encodées, eval, base64_decode ou assert combinés à des variables de requête sont de sérieux signaux d'alerte.

Cherchez aussi dans le journal d'accès les requêtes vers des fichiers PHP situés dans uploads. Un POST vers un tel fichier est rarement légitime :

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

Vous en avez trouvé un ? Conservez les preuves avant de supprimer quoi que ce soit

Notez d'abord les métadonnées, puis copiez le fichier et les journaux. Stockez le tout dans un répertoire privé, en dehors de toute racine web servie par votre serveur. Sur certains hébergements mutualisés, le répertoire personnel lui-même est accessible depuis le web : vérifiez la racine web (document root) dans votre panneau de contrôle ou dans la configuration du 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*

L'horodatage et le suffixe aléatoire réduisent le risque de réutiliser un nom de fichier de preuve ; vérifiez qu'aucun fichier du même nom n'existe avant de refaire la capture. Les hébergeurs font tourner les journaux : copiez-les maintenant. Téléchargez ensuite les preuves vers un emplacement sûr hors du serveur, et notez le chemin du fichier et les lignes de journal correspondantes.

Considérez les identifiants de base de données de wp-config.php comme potentiellement exposés. Évaluez si des données à caractère personnel ont été consultées, divulguées, altérées, perdues ou rendues indisponibles : un shell fonctionnel est un signal grave, mais il n'établit pas à lui seul une violation de données à caractère personnel au sens du RGPD. L'article 33 impose de notifier l'autorité de contrôle (en France, la CNIL) dans les meilleurs délais et, si possible, 72 heures au plus tard après en avoir pris connaissance, à moins que la violation ne soit pas susceptible d'engendrer un risque pour les droits et libertés des personnes. L'article 33, paragraphe 5, impose de documenter toute violation, y compris celles que vous ne notifiez pas. Notre guide sur le délai de 72 heures détaille ce point. Pour le nettoyage complet, suivez notre plan de suppression des malwares.

Bloquer l'exécution de PHP dans le dossier uploads (Apache et nginx)

Un média légitime n'a jamais besoin d'être exécuté comme du PHP. Sur Apache 2.4, créez wp-content/uploads/.htaccess :

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

Apache ne tient compte de ce fichier que si AllowOverride l'autorise pour ce répertoire, ce qui, pour Require, signifie All ou au moins AuthConfig. Si les surcharges sont désactivées, demandez à l'hébergeur de placer le bloc dans une section <Directory> limitée au chemin absolu du répertoire uploads de ce site. Ne le mettez pas sans restriction dans le virtual host : il pourrait bloquer PHP sur tout le site.

Sur nginx, ajoutez ceci au bloc server :

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

nginx commence par chercher la location à préfixe le plus long qui correspond. Si cette location utilise ^~, par exemple location ^~ /wp-content/uploads/, nginx n'évalue jamais les locations à expression régulière, et votre règle est ignorée. Dans ce cas, la règle de refus doit se trouver dans ce bloc. Sinon, les locations à expression régulière sont examinées dans l'ordre et la première qui correspond l'emporte : cette règle doit donc précéder votre bloc général location ~ \.php$. Si WordPress est installé dans un sous-répertoire comme /blog/, ou utilise une URL d'uploads personnalisée, adaptez le chemin. Vérifiez la configuration effective, puis rechargez :

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

Sur un hébergement géré, demandez au support d'appliquer la règle.

Ces règles refusent les requêtes web directes qui correspondent aux extensions listées, lorsque la configuration du serveur les applique. Elles ne couvrent pas tous les suffixes exécutables et n'empêchent pas un autre script PHP d'inclure un fichier envoyé. Elles ne suppriment pas les shells et n'empêchent pas les envois. Demandez à votre hébergeur quels suffixes sont associés à PHP, et complétez la liste si nécessaire.

Vérifier la règle avec un fichier de test

Utilisez un nom de fichier neuf et aléatoire. Avec noclobber activé, la commande refuse d'écraser un fichier existant, et ne supprime qu'un fichier qu'elle a elle-même créé :

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 le corps de la réponse contient marker-42, PHP a exécuté le fichier, puisque cette chaîne n'est pas dans le fichier lui-même. Si vous voyez la ligne <?php brute avec le statut 200, PHP ne s'est pas exécuté, mais le serveur expose du code source. C'est un autre problème, à traiter avec votre hébergeur. Un statut 200 seul ne prouve rien. Un 403 ne compte que s'il vient de votre règle, et non d'un CDN ou d'un WAF. Testez directement l'origine avec curl --resolve example.com:443:ORIGIN_IP, et cherchez « access forbidden by rule » (nginx) ou « client denied by server configuration » (Apache) dans le journal d'erreurs de l'origine.

Droits et propriétaire des fichiers WordPress : vérifier, ne pas réinitialiser

Les droits et le propriétaire décident ensemble de ce que le processus PHP peut modifier. Le propriétaire seul ne suffit pas, car les bits de groupe, les bits « autres » et les ACL accordent aussi l'écriture. N'exécutez pas de chmod récursif sur tout le site. Il peut rendre lisibles par tous des fichiers privés comme les sauvegardes, et retirer le bit d'exécution de scripts de maintenance.

Commencez par identifier qui possède les fichiers et sous quel utilisateur PHP s'exécute :

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

Sur de nombreux hébergements mutualisés, PHP s'exécute sous l'utilisateur de votre propre compte. Dans cette configuration, les bits de lecture seule ordinaires n'offrent qu'une protection limitée : le propriétaire du fichier peut généralement les modifier pour rétablir l'écriture, sauf si d'autres restrictions du système de fichiers ou de l'hébergement l'en empêchent.

Listez ensuite ce que l'utilisateur PHP peut réellement écrire, ainsi que tout ce qui est modifiable par tous. Remplacez PHPUSER par l'utilisateur trouvé :

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

Examinez les deux listes séparément : les chemins modifiables par tous demandent en général une correction, tandis que les chemins modifiables par PHP dépendent de votre modèle de mise à jour. Un déploiement en lecture seule devrait normalement limiter les écritures de PHP à uploads et aux dossiers d'exécution nécessaires ; les mises à jour depuis le tableau de bord et les mises à jour automatiques exigent des droits d'écriture supplémentaires. Corrigez les chemins examinés un par un avec une notation symbolique, par exemple chmod g-w,o-w path/to/file. Retirer des bits ne rend jamais plus permissif un mode existant plus strict. Le guide de durcissement de WordPress considère 755 pour les répertoires et 644 pour les fichiers comme la borne supérieure habituelle. Réglez wp-config.php sur 640 ou 600 selon la manière dont PHP le lit, puis testez le site. N'utilisez jamais 777.

Il reste une limite incontournable. PHP doit pouvoir écrire dans uploads : une extension vulnérable qui s'exécute sous cet utilisateur peut donc toujours y écrire.

define( 'DISALLOW_FILE_EDIT', true ); dans wp-config.php supprime les éditeurs de fichiers du tableau de bord. DISALLOW_FILE_MODS va plus loin, mais désactive aussi les mises à jour automatiques en arrière-plan. Ne l'utilisez que si vous mettez à jour par un autre moyen.

Fermer la faille qui a laissé entrer le shell, puis surveiller uploads

Supprimer le shell ne corrige pas la cause. Tant que la porte d'entrée reste ouverte, une nouvelle infection reste possible.

Comparez la sortie de stat enregistrée avec le journal d'accès. Cherchez le formulaire ou le point d'accès de l'extension qui a reçu un POST peu avant l'apparition du fichier. Mettez alors l'extension à jour, ou reconfigurez-la : restreignez les types de fichiers autorisés et, si l'extension le permet, stockez les pièces jointes en dehors de la racine web. Si aucune mise à jour ni mesure d'atténuation sûre n'existe, supprimez l'extension. La désactiver n'est pas un substitut fiable, car ses fichiers restent sur le disque et certains peuvent rester accessibles directement. Changez ensuite les mots de passe des administrateurs, les clés (salts) de wp-config.php et les identifiants de la base de données.

Continuez à surveiller ensuite :

  • Lancez la commande find combinée dans une tâche cron quotidienne qui vous envoie par e-mail tout résultat.
  • Ajoutez une surveillance de l'intégrité des fichiers.
  • Exécutez régulièrement wp core verify-checksums.

L'extension Security Audit & Compliance de secaudit.eu aide à la surveillance. Elle conserve une référence d'intégrité des fichiers, signale les modifications et le contenu obfusqué, et vérifie les signatures de malwares et les indicateurs de compromission. Elle détecte et signale. Elle ne supprime rien. Pour d'autres signaux d'alerte, voir 12 signes que votre site WordPress a été piraté.

FAQ

Tout fichier PHP dans wp-content/uploads est-il malveillant ?

Non. Les fichiers index.php factices et certains dossiers d'extensions sont légitimes. Notez les métadonnées, inspectez le fichier sans l'exécuter et vérifiez à quelle extension appartient le dossier.

Une extension de sécurité ou un WAF rend-il la règle sur uploads inutile ?

Non. Un pare-feu peut laisser passer un nouveau schéma d'attaque. La règle serveur est une couche distincte qui refuse les requêtes directes vers les extensions listées, même si un envoi a réussi.

Bloquer PHP dans uploads suffit-il si j'ai déjà trouvé un shell ?

Non. Conservez les preuves, nettoyez le site et fermez la porte d'entrée. Le shell a peut-être déjà servi à déposer des fichiers ailleurs ou à lire votre base de données.

Cet article est une information générale et ne constitue pas un conseil juridique.

Vérifiez votre propre site

Security Audit & Compliance est l’extension WordPress de secaudit.eu : elle recherche les logiciels malveillants, le spam caché, les fichiers du cœur modifiés et le code vulnérable, et rattache ses constats à l’article 32 du RGPD, à l’article 21 de NIS2 et à l’OWASP Top 10.

Voir ce que vérifie l’extension
Melike Görel

Melike Görel rédige les guides de secaudit.eu sur la sécurité WordPress et la conformité européenne pour AngelForce OÜ, l’éditeur de logiciels basé à Tallinn qui développe secaudit.eu.

Rédigé avec l’aide de l’IA et vérifié avant publication. Cet article est une information générale et non un conseil juridique.