Web shells no WordPress em uploads: encontrar e bloquear
Encontre web shells em wp-content/uploads com comandos seguros, bloqueie PHP no Apache e nginx, reveja permissões de ficheiros e feche a porta de entrada.
Também em: English Deutsch Français Español Italiano Polski Türkçe
Se suspeita de uma web shell do WordPress em uploads, tem três tarefas distintas pela frente. Encontrar o ficheiro e preservá-lo. Impedir que pedidos diretos executem PHP em wp-content/uploads. Fechar a via que deixou o ficheiro entrar. Cada tarefa resolve um problema diferente e nenhuma, por si só, equivale a uma recuperação completa.
Este artigo apresenta os comandos e as definições de servidor para cada tarefa e explica o que cada uma delas não faz.
O que é uma web shell e porque é que wp-content/uploads é o sítio preferido
Uma web shell é um pequeno script, em regra PHP, que dá a um atacante um painel de controlo remoto no seu servidor. Consoante o servidor, pode permitir-lhe executar comandos, ler o wp-config.php com a palavra-passe da base de dados, consultar a base de dados e carregar mais ficheiros.
Os atacantes gostam de wp-content/uploads porque o WordPress precisa normalmente de acesso de escrita nessa pasta. Ficheiros multimédia, anexos de formulários e exportações de plugins acabam todos ali, e os ficheiros são servidos diretamente por URL. Se um ficheiro PHP aterrar aí e o servidor o executar, qualquer pessoa na internet pode chamá-lo.
Como entram as web shells em wp-content/uploads
As vias de entrada habituais são pouco glamorosas:
- Um formulário de upload que aceita qualquer tipo de ficheiro, como um plugin de formulário de contacto ou de candidaturas que guarda anexos em uploads.
- Uma falha de upload arbitrário de ficheiros num plugin ou tema.
- Controlo de acesso quebrado, em que uma função pensada para administradores pode ser chamada por qualquer pessoa. O relatório Patchstack State of WordPress Security in 2026, que é um relatório de um fornecedor, aponta esta como a categoria mais explorada.
- Uma conta roubada combinada com uma fraqueza. As verificações predefinidas de tipo de ficheiro do WordPress normalmente impedem um Editor de carregar um ficheiro
.php. Uma conta roubada torna-se uma via de upload quando essa função também consegue chegar a uma falha de upload num plugin, ou quando a configuração do site atribui a capacidadeunfiltered_upload. Um Administrador num site simples consegue geralmente instalar plugins e usar os editores de ficheiros do painel.DISALLOW_FILE_EDITdesativa esses editores;DISALLOW_FILE_MODSbloqueia também a instalação e a atualização de plugins e temas a partir do painel. Em multisite, só os Super Administradores têm esses poderes.
As shells raramente se chamam shell.php. Procure extensões duplas como invoice.pdf.php, as extensões alternativas .phtml e .phar, e nomes que imitam ficheiros de cache ou miniaturas. Código PHP escondido dentro de um .jpg é um sinal de alerta, mas não corre só por conter PHP. Só corre se o servidor entregar esse ficheiro ao PHP ou se outro script vulnerável o incluir.
O risco concentra-se nos plugins. A Patchstack atribui aos plugins 91 % das novas vulnerabilidades do ecossistema WordPress em 2025. Para as vulnerabilidades muito exploradas, indica uma mediana ponderada de 5 horas até à exploração em massa. O nosso guia sobre plugins vulneráveis explica como acompanhar esse ritmo.
Encontrar uma web shell no WordPress: ficheiros PHP em uploads
Estes comandos não alteram ficheiros. Execute-os por SSH a partir da raiz do WordPress, a pasta que contém o wp-config.php. Comece pelas extensões óbvias:
find wp-content/uploads -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) -ls
invoice.pdf.php é apanhado porque termina em .php. Para apanhar variantes como .php5 ou image.php.jpg, alargue o padrão para -iname '*.ph*' e conte com mais ruído.
Depois, liste os ficheiros alterados recentemente, ou os ficheiros mais recentes do que um upload que sabe estar limpo:
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
Os atacantes conseguem falsificar datas de modificação, por isso trate-as como indícios. Para encontrar código PHP em ficheiros com nome de imagem:
grep -rli --include='*.jpg' --include='*.jpeg' --include='*.png' --include='*.gif' --include='*.webp' --include='*.ico' '<?php' wp-content/uploads
Tenha em conta que o grep lê o conteúdo dos ficheiros, e ler um ficheiro pode atualizar a sua data de acesso. Se já suspeita de um comprometimento, peça primeiro um snapshot ao seu alojamento.
Nem todos os resultados são maliciosos. Alguns plugins deixam um index.php de substituição que contém apenas um comentário, e alguns plugins de cache ou de formulários escrevem PHP na sua própria subpasta. Antes de abrir um candidato, guarde os respetivos metadados, como descrito na secção seguinte. Depois inspecione-o com less ou xxd file | head -40. Nunca o abra num navegador, porque isso pode executá-lo. Cadeias longas codificadas, eval, base64_decode ou assert combinados com variáveis de pedido são sinais de alerta fortes.
Pesquise também no registo de acessos pedidos a ficheiros PHP em uploads. Um POST para um ficheiro desses raramente é legítimo:
grep -E '"(GET|POST) /wp-content/uploads/[^ ]*\.(php|phtml|phar)' access.log
Encontrou uma? Preserve as provas antes de apagar o que quer que seja
Registe primeiro os metadados e depois copie o ficheiro e os registos. Guarde tudo num diretório privado, fora de qualquer raiz de documentos servida pelo seu servidor. Em alguns alojamentos partilhados a própria pasta pessoal é acessível pela web, por isso confirme a raiz de documentos no painel de controlo ou na configuração do 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*
O carimbo de data/hora e o sufixo aleatório reduzem a probabilidade de reutilizar o nome de um ficheiro de provas; verifique se já existem ficheiros com esse nome antes de repetir a captura. Os alojamentos rodam os registos, por isso copie-os já. Depois transfira as provas para um local seguro fora do servidor e anote o caminho e as linhas de registo correspondentes.
Trate as credenciais da base de dados no wp-config.php como potencialmente expostas. Avalie se houve acesso, divulgação, alteração, perda ou indisponibilização de dados pessoais: uma shell funcional é um aviso grave, mas não estabelece por si só uma violação de dados pessoais nos termos do RGPD. O art. 33.º exige a notificação da autoridade de controlo sem demora injustificada e, sempre que possível, no prazo de 72 horas após ter tido conhecimento de uma violação de dados pessoais, a menos que seja improvável que a violação resulte num risco para os direitos e liberdades das pessoas. O art. 33.º, n.º 5, obriga a documentar todas as violações, incluindo as que não notifica. O nosso guia das 72 horas tem os pormenores. Para a limpeza completa, siga o nosso plano de remoção de malware.
Bloquear a execução de PHP na pasta uploads (Apache e nginx)
Ficheiros multimédia legítimos nunca precisam de ser executados como PHP. No Apache 2.4, crie wp-content/uploads/.htaccess:
<FilesMatch "\.(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
O Apache só respeita este ficheiro se AllowOverride o permitir para esse diretório, o que, para Require, significa All ou, no mínimo, AuthConfig. Se as substituições estiverem desativadas, peça ao alojamento que coloque o bloqueio numa secção <Directory> restrita ao caminho absoluto, no sistema de ficheiros, do diretório de uploads deste site. Não o coloque sem restrição no virtual host, porque isso poderia bloquear o PHP em todo o site.
No nginx, acrescente isto ao bloco server:
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ { deny all; }
O nginx procura primeiro a location de prefixo com a correspondência mais longa. Se essa location usar ^~, por exemplo location ^~ /wp-content/uploads/, o nginx nunca avalia as locations com expressões regulares e a sua regra é ignorada. Nesse caso, a regra de bloqueio tem de ir para dentro desse bloco. Caso contrário, as locations com expressões regulares são verificadas por ordem e vence a primeira correspondência, pelo que esta regra tem de vir antes do seu bloco geral location ~ \.php$. Se o WordPress estiver num subdiretório como /blog/, ou usar um URL de uploads personalizado, ajuste o caminho. Verifique a configuração efetiva e depois recarregue:
sudo nginx -T | grep -n 'location'
sudo nginx -t && sudo systemctl reload nginx
Em alojamento gerido, peça ao suporte que aplique a regra.
Estas regras negam pedidos web diretos que correspondam às extensões listadas, quando a configuração do servidor as aplica. Não cobrem todos os sufixos executáveis nem impedem que outro script PHP inclua um ficheiro carregado. Não apagam shells nem impedem uploads. Pergunte ao seu alojamento que sufixos estão associados ao PHP e alargue a lista, se necessário.
Verificar a regra com um ficheiro de teste
Use um nome de ficheiro novo e aleatório. Com noclobber ativo, o comando recusa-se a substituir um ficheiro existente e só apaga um ficheiro que ele próprio criou:
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
Se o corpo da resposta contiver marker-42, o PHP executou o ficheiro, porque essa cadeia não está no próprio ficheiro. Se vir a linha <?php em bruto com estado 200, o PHP não correu, mas o servidor está a expor código-fonte. É um problema distinto, para o seu alojamento resolver. Um estado 200, só por si, não prova nada. Um 403 só conta se tiver sido a sua regra a produzi-lo, e não uma CDN ou uma WAF. Teste diretamente contra a origem com curl --resolve example.com:443:ORIGIN_IP e procure "access forbidden by rule" (nginx) ou "client denied by server configuration" (Apache) no registo de erros da origem.
Permissões e propriedade de ficheiros no WordPress: rever, não repor
Permissões e propriedade decidem, em conjunto, o que o processo PHP pode alterar. A propriedade, sozinha, não resolve a questão, porque os bits de grupo, os bits de «outros» e as ACL também concedem escrita. Não execute um chmod recursivo em todo o site. Pode tornar legíveis por todos ficheiros privados como cópias de segurança e pode retirar o bit de execução a scripts de manutenção.
Primeiro, descubra quem é o proprietário dos ficheiros e com que utilizador o PHP corre:
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
Em muitos alojamentos partilhados, o PHP corre com o utilizador da sua própria conta. Nesse cenário, os bits de modo só de leitura oferecem uma proteção limitada: o proprietário do ficheiro pode, em geral, alterá-los para recuperar a escrita, a menos que outras restrições do sistema de ficheiros ou do alojamento o impeçam.
A seguir, liste o que o utilizador do PHP consegue realmente escrever, mais tudo o que tenha escrita para todos. Substitua PHPUSER pelo utilizador que encontrou:
sudo -u PHPUSER find . -writable ! -type l -print
find . -perm -o+w ! -type l -ls
Reveja as duas listas em separado: os caminhos com escrita para todos precisam, em geral, de correção, enquanto os caminhos onde o PHP pode escrever dependem do seu modelo de atualização. Uma implementação só de leitura deve normalmente limitar as escritas do PHP a uploads e às pastas de execução necessárias; as atualizações pelo painel e as automáticas exigem acesso de escrita adicional. Corrija os caminhos revistos um a um, com remoção simbólica, por exemplo chmod g-w,o-w path/to/file. Remover bits nunca afrouxa um modo existente mais restritivo. O guia de hardening do WordPress trata 755 para diretórios e 644 para ficheiros como o limite superior habitual. Defina o wp-config.php como 640 ou 600, consoante a forma como o PHP o lê, e teste o site depois disso. Nunca use 777.
Há um limite que não se contorna. O PHP tem de poder escrever em uploads, por isso um plugin vulnerável a correr com esse utilizador continua a poder escrever lá.
define( 'DISALLOW_FILE_EDIT', true ); no wp-config.php remove os editores de ficheiros do painel. DISALLOW_FILE_MODS vai mais longe, mas desliga também as atualizações automáticas em segundo plano. Só o use se atualizar de outra forma.
Feche a falha que deixou a shell entrar e continue a vigiar uploads
Apagar a shell não corrige a causa. Enquanto a via de entrada se mantiver aberta, a reinfeção continua possível.
Compare a saída guardada do stat com o registo de acessos. Procure o formulário ou o endpoint do plugin que recebeu um POST pouco antes de o ficheiro aparecer. Depois atualize o plugin ou reconfigure-o: restrinja os tipos de ficheiro permitidos e guarde os anexos fora da raiz web, se o plugin o suportar. Se não houver atualização nem mitigação segura, elimine o plugin. A desativação não é um substituto fiável, porque os ficheiros continuam no disco e alguns podem ainda estar diretamente acessíveis. A seguir, mude as palavras-passe dos administradores, os salts do wp-config.php e as credenciais da base de dados.
Depois, continue a vigiar:
- Execute o comando
findcombinado numa tarefa cron diária que lhe envie por e-mail qualquer resultado. - Adicione monitorização de integridade de ficheiros.
- Execute
wp core verify-checksumscom regularidade.
O plugin Security Audit & Compliance da secaudit.eu ajuda na monitorização. Mantém uma base de referência de integridade de ficheiros, comunica alterações e conteúdo ofuscado, e verifica assinaturas de malware e indicadores de comprometimento. Deteta e comunica. Não remove nada. Para outros sinais de alerta, consulte 12 sinais de que o seu site WordPress foi pirateado.
FAQ
Todos os ficheiros PHP em wp-content/uploads são maliciosos?
Não. Ficheiros index.php de substituição e algumas pastas de plugins são legítimos. Registe os metadados, inspecione o ficheiro sem o executar e verifique a que plugin pertence a pasta.
Um plugin de segurança ou uma WAF dispensa a regra para uploads?
Não. Uma firewall pode falhar um novo padrão de ataque. A regra no servidor é uma camada independente, que nega pedidos diretos às extensões listadas mesmo quando um upload passa.
Bloquear PHP em uploads chega, se já encontrei uma shell?
Não. Preserve as provas, limpe o site e feche a via de entrada. A shell pode já ter sido usada para plantar ficheiros noutros locais ou para ler a sua base de dados.
Este artigo é uma informação geral e não constitui aconselhamento jurídico.
Verifique o seu site
O Security Audit & Compliance é o plugin WordPress da secaudit.eu: procura malware, spam oculto, ficheiros do núcleo alterados e código vulnerável, e associa as conclusões ao art. 32.º do RGPD, ao art. 21.º da NIS2 e ao OWASP Top 10.
Ver o que o plugin verifica