Web shell w WordPressie w uploads: wykrywanie i blokada
Jak znaleźć web shell w wp-content/uploads w WordPressie: bezpieczne polecenia, blokada PHP w Apache i nginx, uprawnienia plików i zamknięcie luki.
Także po: English Deutsch Français Español Italiano Português Türkçe
Jeśli podejrzewają Państwo web shell w WordPressie ukryty w katalogu uploads, do wykonania są trzy osobne zadania. Trzeba znaleźć plik i go zabezpieczyć. Trzeba uniemożliwić bezpośrednie uruchamianie PHP w wp-content/uploads. Trzeba też zamknąć drogę, którą plik się tam dostał. Każde z tych zadań rozwiązuje inny problem i żadne z nich samo nie oznacza pełnego odzyskania kontroli nad stroną.
Poniżej znajdą Państwo polecenia i ustawienia serwera dla każdego z tych zadań oraz informację, czego dane zadanie nie załatwia.
Czym jest web shell i dlaczego wp-content/uploads to jego ulubione miejsce
Web shell to niewielki skrypt, najczęściej w PHP, który daje atakującemu zdalny panel sterowania na serwerze. W zależności od konfiguracji pozwala uruchamiać polecenia, odczytać wp-config.php z hasłem do bazy danych, wysyłać zapytania do bazy i wgrywać kolejne pliki.
Atakujący lubią wp-content/uploads, ponieważ WordPress zwykle musi mieć tam prawo zapisu. Trafiają tam pliki multimedialne, załączniki z formularzy i eksporty wtyczek, a zawartość katalogu jest serwowana bezpośrednio pod adresem URL. Jeśli plik PHP znajdzie się w tym miejscu, a serwer go wykona, może go wywołać każdy użytkownik internetu.
Jak web shelle trafiają do wp-content/uploads
Najczęstsze drogi są mało efektowne:
- Formularz przyjmujący pliki dowolnego typu, na przykład wtyczka formularza kontaktowego lub rekrutacyjnego, która zapisuje załączniki w uploads.
- Błąd w wtyczce lub motywie pozwalający wgrać dowolny plik.
- Błędna kontrola dostępu, czyli funkcja przeznaczona dla administratorów, którą może wywołać każdy. Raport Patchstack State of WordPress Security in 2026, będący raportem dostawcy, wskazuje tę kategorię jako najczęściej wykorzystywaną.
- Przejęte konto w połączeniu z inną słabością. Domyślne kontrole typów plików w WordPressie zwykle nie pozwalają Redaktorowi wgrać pliku
.php. Przejęte konto staje się drogą wgrania pliku, gdy ta sama rola ma dostęp do błędu w uploadzie wtyczki albo gdy konfiguracja serwisu nadaje uprawnienieunfiltered_upload. Administrator pojedynczej witryny zwykle może instalować wtyczki i korzystać z edytorów plików w panelu.DISALLOW_FILE_EDITwyłącza te edytory, aDISALLOW_FILE_MODSblokuje dodatkowo instalację i aktualizację wtyczek oraz motywów z poziomu panelu. W instalacji multisite takie uprawnienia mają tylko Super Administratorzy.
Shelle rzadko nazywają się shell.php. Należy szukać podwójnych rozszerzeń, jak invoice.pdf.php, alternatywnych rozszerzeń .phtml i .phar oraz nazw udających pliki cache lub miniatury. Kod PHP ukryty w pliku .jpg jest sygnałem ostrzegawczym, ale sam z siebie się nie uruchomi. Wykona się tylko wtedy, gdy serwer przekaże ten plik do PHP albo gdy dołączy go inny podatny skrypt.
Ryzyko koncentruje się we wtyczkach. Patchstack przypisuje wtyczkom 91 % nowych podatności w ekosystemie WordPressa w 2025 roku. Dla intensywnie wykorzystywanych podatności podaje ważoną medianę 5 godzin do masowej eksploatacji. Nasz przewodnik o podatnych wtyczkach pokazuje, jak nadążyć za takim tempem.
Wykrywanie web shella w WordPressie: pliki PHP w uploads
Poniższe polecenia nie zmieniają plików. Uruchamia się je przez SSH z katalogu głównego WordPressa, czyli tego, w którym leży wp-config.php. Na początek oczywiste rozszerzenia:
find wp-content/uploads -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) -ls
Plik invoice.pdf.php zostanie znaleziony, bo kończy się na .php. Żeby wychwycić warianty typu .php5 lub image.php.jpg, można poszerzyć wzorzec do -iname '*.ph*', licząc się z większą liczbą fałszywych trafień.
Dalej: pliki zmienione niedawno albo nowsze od wgranego pliku, o którym wiadomo, że jest czysty:
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
Atakujący potrafią podrabiać daty modyfikacji, więc to tylko wskazówki. Kod PHP w plikach o nazwach obrazów wykryje to polecenie:
grep -rli --include='*.jpg' --include='*.jpeg' --include='*.png' --include='*.gif' --include='*.webp' --include='*.ico' '<?php' wp-content/uploads
grep czyta zawartość plików, a odczyt może zaktualizować czas dostępu. Jeśli podejrzenie włamania jest już uzasadnione, najpierw warto poprosić hostingodawcę o snapshot.
Nie każde trafienie jest złośliwe. Niektóre wtyczki zostawiają zastępczy index.php zawierający tylko komentarz, a część wtyczek cache lub formularzy zapisuje PHP we własnym podkatalogu. Zanim otworzy się podejrzany plik, trzeba zapisać jego metadane, jak opisano w następnej sekcji. Dopiero potem można go obejrzeć przez less lub xxd file | head -40. Nigdy nie należy otwierać go w przeglądarce, bo może to go uruchomić. Mocnymi sygnałami ostrzegawczymi są długie zakodowane ciągi znaków oraz eval, base64_decode lub assert w połączeniu ze zmiennymi z żądania.
Warto też przeszukać log dostępu pod kątem żądań do plików PHP w uploads. POST do takiego pliku rzadko bywa uzasadniony:
grep -E '"(GET|POST) /wp-content/uploads/[^ ]*\.(php|phtml|phar)' access.log
Znaleziony plik: najpierw zabezpieczenie dowodów, potem usuwanie
Najpierw zapisuje się metadane, potem kopiuje plik i logi. Całość należy przechowywać w prywatnym katalogu poza wszystkimi katalogami głównymi (document root) obsługiwanymi przez serwer. Na niektórych hostingach współdzielonych sam katalog domowy jest dostępny z sieci, dlatego trzeba sprawdzić document root w panelu hostingu lub w konfiguracji vhosta.
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*
Znacznik czasu i losowy sufiks zmniejszają ryzyko ponownego użycia nazwy pliku z dowodami. Przed powtórzeniem zapisu trzeba sprawdzić, czy takie nazwy już nie istnieją. Hostingodawcy rotują logi, więc kopię należy zrobić od razu. Następnie dowody trzeba pobrać w bezpieczne miejsce poza serwerem i zanotować ścieżkę oraz pasujące wiersze logów.
Dane dostępowe do bazy w wp-config.php należy traktować jako potencjalnie ujawnione. Trzeba ocenić, czy doszło do dostępu do danych osobowych, ich ujawnienia, zmiany, utraty lub niedostępności. Działający shell jest poważnym sygnałem ostrzegawczym, ale sam w sobie nie przesądza o naruszeniu ochrony danych osobowych w rozumieniu RODO. Art. 33 wymaga zgłoszenia organowi nadzorczemu bez zbędnej zwłoki, jeżeli to możliwe, w ciągu 72 godzin od stwierdzenia naruszenia, chyba że jest mało prawdopodobne, by skutkowało ono ryzykiem naruszenia praw lub wolności osób fizycznych. Art. 33 ust. 5 nakłada obowiązek dokumentowania każdego naruszenia, także tych, których się nie zgłasza. Szczegóły opisuje nasz przewodnik o terminie 72 godzin. Pełne czyszczenie strony opisuje nasz plan usuwania malware.
Blokada wykonywania PHP w katalogu uploads (Apache i nginx)
Prawidłowe pliki multimedialne nigdy nie muszą być uruchamiane jako PHP. W Apache 2.4 należy utworzyć plik wp-content/uploads/.htaccess:
<FilesMatch "\.(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
Apache uwzględni ten plik tylko wtedy, gdy AllowOverride na to pozwala dla danego katalogu, co w przypadku Require oznacza All lub co najmniej AuthConfig. Jeśli nadpisywanie jest wyłączone, trzeba poprosić hostingodawcę o umieszczenie reguły w sekcji <Directory> ograniczonej do bezwzględnej ścieżki systemowej katalogu uploads tej witryny. Nie należy wstawiać jej bez ograniczenia zakresu do konfiguracji wirtualnego hosta, bo mogłoby to zablokować PHP w całej witrynie.
W nginx wystarczy dodać do bloku server:
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ { deny all; }
nginx najpierw wybiera lokalizację z najdłuższym pasującym prefiksem. Jeśli ta lokalizacja używa ^~, na przykład location ^~ /wp-content/uploads/, nginx w ogóle nie sprawdza lokalizacji z wyrażeniami regularnymi i reguła zostanie zignorowana. W takim przypadku reguła deny musi trafić do tego bloku. W przeciwnym razie lokalizacje regex są sprawdzane po kolei i wygrywa pierwsza pasująca, więc reguła musi stać przed ogólnym blokiem location ~ \.php$. Jeśli WordPress działa w podkatalogu, na przykład /blog/, albo ma własny adres URL uploads, ścieżkę trzeba dostosować. Należy sprawdzić efektywną konfigurację, a potem przeładować serwer:
sudo nginx -T | grep -n 'location'
sudo nginx -t && sudo systemctl reload nginx
Na hostingu zarządzanym regułę powinno wdrożyć wsparcie techniczne.
Te reguły odrzucają bezpośrednie żądania z wymienionymi rozszerzeniami, o ile konfiguracja serwera je stosuje. Nie obejmują każdego rozszerzenia, które może zostać wykonane, i nie powstrzymają innego skryptu PHP przed dołączeniem wgranego pliku. Nie usuwają też shelli ani nie zapobiegają wgrywaniu plików. Warto zapytać hostingodawcę, jakie rozszerzenia są mapowane na PHP, i w razie potrzeby rozszerzyć listę.
Sprawdzenie reguły za pomocą pliku testowego
Należy użyć nowej, losowej nazwy pliku. Przy ustawionym noclobber polecenie odmówi nadpisania istniejącego pliku i usunie wyłącznie plik, który samo utworzyło:
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
Jeśli w odpowiedzi pojawi się marker-42, PHP wykonał plik, bo tego ciągu nie ma w samym pliku. Jeśli zobaczą Państwo surową linię <?php ze statusem 200, PHP się nie uruchomił, ale serwer ujawnia kod źródłowy. To osobny problem po stronie hostingodawcy. Sam status 200 niczego nie dowodzi. Status 403 się liczy tylko wtedy, gdy wygenerowała go Państwa reguła, a nie CDN czy WAF. Test należy wykonać na serwerze źródłowym, używając curl --resolve example.com:443:ORIGIN_IP, i poszukać „access forbidden by rule” (nginx) lub „client denied by server configuration” (Apache) w logu błędów serwera źródłowego.
Uprawnienia i właściciel plików w WordPressie: przegląd zamiast resetu
O tym, co proces PHP może zmieniać, decydują łącznie uprawnienia i właściciel. Sam właściciel nie rozstrzyga sprawy, bo prawo zapisu dają też bity grupy, bity „others” i listy ACL. Nie należy uruchamiać rekurencyjnego chmod na całej witrynie. Może to uczynić pliki prywatne, takie jak kopie zapasowe, czytelnymi dla wszystkich i odebrać bit wykonywania skryptom konserwacyjnym.
Najpierw trzeba ustalić, kto jest właścicielem plików i jako jaki użytkownik działa 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
Na wielu hostingach współdzielonych PHP działa jako użytkownik konta hostingowego. W takiej konfiguracji zwykłe bity trybu tylko do odczytu dają ograniczoną ochronę: właściciel pliku zazwyczaj może je zmienić i przywrócić prawo zapisu, o ile nie blokują tego inne ograniczenia systemu plików lub hostingu.
Następnie należy wypisać to, co użytkownik PHP faktycznie może zapisywać, oraz wszystko, co jest zapisywalne dla wszystkich. PHPUSER trzeba zastąpić ustalonym użytkownikiem:
sudo -u PHPUSER find . -writable ! -type l -print
find . -perm -o+w ! -type l -ls
Obie listy należy przejrzeć osobno: ścieżki zapisywalne dla wszystkich z reguły wymagają poprawy, a ścieżki zapisywalne dla PHP zależą od przyjętego modelu aktualizacji. Wdrożenie tylko do odczytu powinno zwykle ograniczać zapis PHP do uploads i niezbędnych katalogów roboczych, a aktualizacje z panelu i automatyczne wymagają dodatkowego prawa zapisu. Przejrzane ścieżki poprawia się pojedynczo, usuwając bity symbolicznie, na przykład chmod g-w,o-w path/to/file. Usuwanie bitów nigdy nie poluzuje surowszego istniejącego trybu. Poradnik hardeningu WordPressa uznaje 755 dla katalogów i 644 dla plików za typową górną granicę. wp-config.php należy ustawić na 640 lub 600, zależnie od tego, jak PHP go odczytuje, a potem przetestować stronę. Nigdy 777.
Jest tu ograniczenie, którego nie da się obejść. PHP musi móc zapisywać w uploads, więc podatna wtyczka działająca jako ten użytkownik nadal może tam zapisywać pliki.
Wiersz define( 'DISALLOW_FILE_EDIT', true ); w wp-config.php usuwa edytory plików z panelu. DISALLOW_FILE_MODS idzie dalej, ale wyłącza też automatyczne aktualizacje w tle. Należy go używać tylko wtedy, gdy aktualizacje wykonuje się inną drogą.
Zamknięcie luki, którą wszedł shell, i dalszy nadzór nad uploads
Usunięcie shella nie usuwa przyczyny. Dopóki punkt wejścia jest otwarty, ponowne zainfekowanie pozostaje możliwe.
Zapisany wynik stat trzeba porównać z logiem dostępu. Należy poszukać formularza lub punktu końcowego wtyczki, który otrzymał POST krótko przed pojawieniem się pliku. Potem trzeba zaktualizować wtyczkę lub zmienić jej konfigurację: ograniczyć dozwolone typy plików i, jeśli wtyczka to umożliwia, przechowywać załączniki poza katalogiem głównym serwera WWW. Jeśli nie ma bezpiecznej aktualizacji ani środka zaradczego, wtyczkę należy usunąć. Dezaktywacja nie jest niezawodnym zamiennikiem, bo pliki wtyczki zostają na dysku i część z nich może być nadal bezpośrednio dostępna. Następnie trzeba zmienić hasła administratorów, salty w wp-config.php i dane dostępowe do bazy.
Potem warto dalej pilnować katalogu:
- Uruchamiać połączone polecenie
findw codziennym zadaniu cron, które wysyła e-mailem każdy wynik. - Dodać monitoring integralności plików.
- Regularnie uruchamiać
wp core verify-checksums.
W monitoringu pomaga wtyczka Security Audit & Compliance z secaudit.eu. Prowadzi bazową listę integralności plików, zgłasza zmiany i zaciemnioną zawartość oraz sprawdza sygnatury malware i wskaźniki włamania. Wykrywa i raportuje, niczego nie usuwa. Inne sygnały ostrzegawcze opisuje artykuł 12 oznak, że Twoja strona WordPress została zhakowana.
FAQ
Czy każdy plik PHP w wp-content/uploads jest złośliwy?
Nie. Zastępcze pliki index.php i niektóre katalogi wtyczek są prawidłowe. Należy zapisać metadane, obejrzeć plik bez jego uruchamiania i sprawdzić, do której wtyczki należy katalog.
Czy wtyczka bezpieczeństwa lub WAF czyni regułę dla uploads zbędną?
Nie. Zapora może przeoczyć nowy wzorzec ataku. Reguła serwera to osobna warstwa, która odrzuca bezpośrednie żądania o wymienionych rozszerzeniach, nawet jeśli upload się powiódł.
Czy blokada PHP w uploads wystarczy, jeśli shell już znaleziono?
Nie. Trzeba zabezpieczyć dowody, wyczyścić stronę i zamknąć punkt wejścia. Shell mógł już posłużyć do podrzucenia plików w innych miejscach lub odczytania bazy danych.
Ten artykuł ma charakter informacyjny i nie stanowi porady prawnej.
Sprawdź swoją stronę
Security Audit & Compliance to wtyczka WordPress od secaudit.eu: wyszukuje złośliwe oprogramowanie, ukryty spam, zmienione pliki rdzenia i podatny kod oraz przypisuje wyniki do art. 32 RODO, art. 21 NIS2 i OWASP Top 10.
Zobacz, co sprawdza wtyczka