WordPress znowu zainfekowany? Znajdź backdoor
WordPress znowu zainfekowany po czyszczeniu? Sprawdź, gdzie ukrywają się backdoory: auto_prepend_file, mu-plugins, fałszywe wtyczki, cron, sąsiednie strony.
Także po: English Deutsch Français Español Italiano Português Türkçe
Jeśli WordPress zostaje zainfekowany ponownie mimo czyszczenia, atakujący prawie na pewno nadal ma do niego dostęp. Albo przetrwał backdoor, albo luka, z której skorzystał za pierwszym razem, wciąż jest otwarta. Świeża kopia rdzenia i kilka usuniętych plików nie rozwiązują żadnego z tych problemów.
Ten poradnik zakłada, że czyszczenie już się odbyło. Przechodzi przez miejsca, w których zwykle coś po nim zostaje, w kolejności wykonywania kodu, i do każdego podaje jedną konkretną kontrolę. Kończy się krokiem, który naprawdę przerywa błędne koło.
Dlaczego WordPress jest atakowany ponownie po czyszczeniu
Przyczyny są dwie i często występują razem. Pierwsza to trwałe zagnieżdżenie się atakującego. Rzadko zostawia on jeden backdoor. Instaluje kilka, w różnych miejscach, żeby po znalezieniu jednego pozostałe nadal działały. Druga to wciąż otwarty punkt wejścia: podatna wtyczka, skradzione hasło albo sąsiednia strona na tym samym koncie hostingowym.
Wskazówką jest czas. Ponowna infekcja w ciągu kilku godzin od czyszczenia zwykle oznacza ocalały backdoor albo zaplanowane zadanie, które odtwarza pliki. Infekcja po kilku dniach lub tygodniach wskazuje raczej na otwartą lukę, którą ktoś (albo jakiś bot) znalazł ponownie.
Otwarte luki są znajdowane szybko. State of WordPress Security in 2026 firmy Patchstack to raport producenta, więc trzeba go czytać z tą świadomością. Wynika z niego, że w przypadku mocno wykorzystywanych luk ważona mediana czasu do masowej eksploatacji wyniosła 5 godzin. Około połowa luk o dużym wpływie była wykorzystywana w ciągu 24 godzin. Ten sam raport podaje, że 46 % luk nie miało poprawki w chwili publicznego ujawnienia. Niezałatana wtyczka nie jest drobnym ryzykiem, którym można zająć się w przyszłym tygodniu.
Zanim zacznie się szukać: zabezpieczyć dowody
Przed usunięciem czegokolwiek więcej trzeba wykonać pełną kopię plików i bazy danych oraz zapisać logi dostępowe serwera WWW. Następnie należy zapisać znaczniki czasu plików, żeby później zestawić zmiany z wpisami w logach:
cd /path/to/site
find . -type f -printf '%T@ %TY-%Tm-%Td %TH:%TM %p\n' | sort -n > ~/file-times-$(date +%F).txt
wp db export ~/evidence-db-$(date +%F).sql
Eksport bazy zawiera dane osobowe, dlatego te kopie trzeba przenieść poza serwer, do bezpiecznego miejsca. Inaczej wyszukiwanie kopii zapasowych opisane niżej wyświetli je na liście i ktoś mógłby je przypadkiem odczytać albo usunąć. Warto prowadzić prostą listę każdego usuniętego pliku, użytkownika i zadania cron, z datą. Będzie potrzebna do ustalenia punktu wejścia. Jeśli naruszenie dotyczyło danych osobowych, jest potrzebna także z innego powodu: art. 33 ust. 5 RODO wymaga dokumentowania każdego naruszenia, również tych, których się nie zgłasza. Pełne kroki ograniczania skutków opisuje nasz plan usuwania malware z WordPressa.
Backdoory uruchamiane przed załadowaniem WordPressa
Ustawienie PHP auto_prepend_file uruchamia wskazany plik przed każdym żądaniem PHP, zanim załaduje się WordPress i zanim zobaczy go jakakolwiek wtyczka bezpieczeństwa. Właśnie dlatego jest tak lubiane przez atakujących. Można je ustawić w php.ini, w pliku .user.ini w dowolnym katalogu albo przez php_value w .htaccess na Apache z mod_php. Przeszukać trzeba całe konto:
grep -Rn --include='*.ini' --include='.htaccess' -E 'auto_(prepend|append)_file' ~ 2>/dev/null
php --ini
php --ini pokazuje, które pliki konfiguracyjne ładuje PHP w wierszu poleceń. PHP serwera WWW może używać innych, więc trzeba też sprawdzić ustawienia PHP w panelu hostingu. Każda wartość wskazująca na nieznany plik to znalezisko. Należy zanotować ścieżkę, zachować kopię tego pliku, a dopiero potem usunąć ustawienie. Zmiany w .user.ini mogą zacząć działać dopiero po kilku minutach, ponieważ PHP buforuje te pliki.
Dalej wp-config.php. W czystym pliku blisko końca jest jedno require_once ABSPATH . 'wp-settings.php';, a nad początkowym blokiem komentarza nie ma nic wykonywalnego. Szukamy wszystkiego innego:
head -n 5 wp-config.php
grep -nE 'eval\(|base64_decode|gzinflate|str_rot13|include|require' wp-config.php
awk 'length > 300 {print FILENAME": line "NR" is "length" chars"}' wp-config.php
Potem plik trzeba porównać za pomocą diff z wersją, o której wiadomo, że była czysta, np. ze starej kopii zapasowej. Jeśli takiej kopii nie ma, można porównać jego strukturę z wp-config-sample.php. Dodatkowe include'y, bardzo długie linie i kod przed blokiem komentarza nie powinny się tam znajdować.
Kryjówki w wp-content: mu-plugins i fałszywe wtyczki
Wtyczki must-use i drop-iny
Wszystko, co leży w wp-content/mu-plugins, uruchamia się automatycznie przy każdym żądaniu. WordPress pokazuje te wtyczki tylko w osobnej zakładce Must-Use na ekranie Wtyczki, pliki PHP w podfolderach nie są wyświetlane wcale, a żadnej z nich nie da się dezaktywować z poziomu panelu. Wiele stron nie ma żadnych wtyczek must-use. Niektórzy dostawcy hostingu instalują jedną, do cache'owania lub zarządzania.
ls -la wp-content/mu-plugins
wp plugin list --status=must-use
wp plugin list --status=dropin
Warto sprawdzić także drop-iny: object-cache.php, advanced-cache.php i db.php, leżące bezpośrednio w wp-content. Są uprawnione, jeśli umieściła je tam wtyczka cache lub hosting. Jeśli nic, co zainstalowano, tego nie tłumaczy, należy uznać je za podejrzane.
Fałszywe foldery wtyczek i łudząco podobne nazwy
Folder wtyczki bez poprawnego nagłówka nie pojawia się w WordPressie, ale jego pliki PHP nadal można wywołać bezpośrednio przez URL. Trzeba porównać to, co jest na dysku, z tym, co zna WordPress:
ls -1 wp-content/plugins | sort > on-disk.txt
wp plugin list --field=name | sort > registered.txt
comm -23 on-disk.txt registered.txt
W wyniku pojawią się index.php i wtyczki jednoplikowe, takie jak hello.php. Wszystko inne wymaga sprawdzenia. Uwagę zwracają nazwy naśladujące prawdziwe wtyczki: dodatkowy myślnik, numer wersji w nazwie folderu albo „wp-security-core”. Następnie wp plugin verify-checksums --all. Polecenie obejmuje tylko wtyczki hostowane w WordPress.org. W przypadku wtyczek premium trzeba pobrać świeżą kopię od producenta i porównać ją przez diff -r. Dla rdzenia WordPressa warto skorzystać z naszego poradnika o weryfikacji sum kontrolnych WordPressa.
Niedawno zmienione pliki PHP
find wp-content -type f -name '*.php' -mtime -30 -printf '%TY-%Tm-%Td %p\n' | sort
find wp-content -type f -name '*.php' -ctime -30
Jeśli rdzeń nie został jeszcze zainstalowany od nowa, kolejnym użytecznym punktem odniesienia jest -newer wp-includes/version.php. Atakujący potrafią podrobić czas modyfikacji poleceniem touch, ale nie da się łatwo podrobić czasu zmiany (-ctime), więc należy uruchomić oba polecenia. Pliki PHP w katalogu uploads omawiamy osobno w poradniku o web shellach w wp-content/uploads.
Trwałe zagnieżdżenie w bazie danych i harmonogramie: cron, administratorzy, hasła aplikacji
Zdarzenia WP-Cron znajdują się w bazie danych i przeżywają czyszczenie plików. Oto jak je wylistować:
wp cron event list --fields=hook,next_run_relative,recurrence
Każdy hook powinien należeć do rdzenia albo do znanej wtyczki. Nieznany hook warto wyszukać w kodzie: grep -Rn "hook_name" wp-content. Jeśli kod, który go rejestruje, leży w mu-plugin albo w pliku, którego pochodzenia nie da się wyjaśnić, to znaleziono backdoor. Hook bez pasującego kodu może oznaczać, że backdoor usunięto, a zdarzenie zostało. Zdarzenie usuwa się poleceniem wp cron event delete hook_name.
Potem strona serwerowa: crontab -l dla użytkownika hostingowego oraz sekcja cron w panelu hostingu. Zadanie wywołujące curl lub wget albo zapisujące coś w katalogu głównym witryny może co noc po cichu odtwarzać backdoor.
Dalej konta użytkowników. Poniższe polecenia wypisują wszystkich administratorów, a potem hasła aplikacji każdego z nich:
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
for id in $(wp user list --role=administrator --field=ID); do echo "User $id"; wp user application-password list "$id"; done
Hasła aplikacji działają nadal po zmianie hasła, więc nieznany wpis jest tak samo poważny jak nieznany administrator. Pełny przegląd opisuje nasz poradnik o rolach użytkowników WordPressa.
Na koniec szukamy wstrzykniętych skryptów w treściach i ustawieniach. Jeśli prefiks tabel jest inny, trzeba zamienić wp_:
wp db search 'script src=' wp_options wp_posts --stats
wp db search 'atob(' wp_options wp_posts --stats
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload IN ('yes','on','auto','auto-on') ORDER BY size DESC LIMIT 20;"
Dwa pierwsze wyszukiwania znajdują zewnętrzne skrypty i dekodowanie base64, którego często używa wstrzyknięty JavaScript. Zapytanie wypisuje największe opcje ładowane automatycznie. Nieznana nazwa opcji z dużą wartością zasługuje na bliższe spojrzenie.
Wtyczka audytowa z secaudit.eu zbiera te wskaźniki naruszenia w jednym miejscu: ukrytych administratorów, podejrzane wpisy cron, spam w bazie danych i niezgodności sum kontrolnych rdzenia. Wykrywa i raportuje. Decyzje o usuwaniu pozostają po Państwa stronie → secaudit.eu
Reszta konta hostingowego: sąsiednie strony i stare kopie
Hosting współdzielony często obsługuje kilka stron pod jednym użytkownikiem systemowym. Zainfekowana kopia stagingowa, zapomniana instalacja w /old/ lub /test/ albo archiwum ZIP z kopią zapasową w katalogu głównym witryny zainfekuje ponownie stronę, którą właśnie oczyszczono. Trzeba wylistować wszystko, co jest na koncie:
find ~ -name wp-config.php 2>/dev/null
find ~ -type d -name wp-includes 2>/dev/null
find ~ -type f \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.sql' \) 2>/dev/null
Wszystkie strony na koncie należy oczyścić jednocześnie albo usunąć te, których nikt nie potrzebuje. Jeśli wszystkie strony współdzielą jednego użytkownika SFTP i jednego użytkownika bazy danych, każda zainfekowana strona może zapisywać dane we wszystkich pozostałych. Tam, gdzie to możliwe, trzeba rozdzielić je na osobnych użytkowników albo osobne konta hostingowe.
Zamknąć punkt wejścia i wymienić wszystkie dane dostępowe naraz
Punkt wejścia trzeba znaleźć w logach dostępowych w okolicach znacznika czasu pierwszej infekcji. Szukamy żądań POST do nietypowych plików i do endpointów podejrzewanej wtyczki:
grep '"POST ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -30
Podatną wtyczkę należy zaktualizować. Jeśli poprawki nie ma albo wtyczka jest porzucona, trzeba ją zastąpić. Jeśli nie jest potrzebna, trzeba ją usunąć. Dezaktywacja nie wystarczy, bo pliki zostają na dysku i nadal można je wywołać przez URL.
Następnie wszystkie dane dostępowe wymienia się w jednej sesji, po czyszczeniu, a nie przed nim. Przy wcześniejszej wymianie ocalały backdoor po prostu odczyta nowe wartości.
- Hasła wszystkich administratorów WordPressa i każde hasło aplikacji
- Hasło do bazy danych: zmiana w panelu hostingu, potem aktualizacja
DB_PASSWORDw wp-config.php - Logowanie do panelu hostingu, hasła SFTP i klucze SSH (nieznane klucze usunąć)
- Klucze API zapisane we wtyczkach (płatności, SMTP, CDN, przestrzeń na kopie zapasowe)
- Klucze bezpieczeństwa i sole:
wp config shuffle-salts
Te same kontrole należy powtórzyć po 24 godzinach i ponownie po 7 dniach, a wyniki porównać z zapisanymi dowodami. Jeśli wróci mu-plugin, zdarzenie cron lub zmodyfikowany plik, wciąż jest coś, czego nie znaleziono.
FAQ
Kiedy przestać radzić sobie samodzielnie?
Przy drugiej ponownej infekcji, a od razu wtedy, gdy mogły zostać ujawnione dane klientów albo strona jest sklepem z zamówieniami. Wówczas trzeba skorzystać z profesjonalnej pomocy przy reagowaniu na incydenty. Jeśli naruszenie dotyczy danych osobowych, trzeba też sprawdzić, czy obowiązuje 72-godzinny termin zgłoszenia do organu nadzorczego (w Polsce: Prezesa UODO) wynikający z RODO. Nasz poradnik o zgłoszeniu naruszenia w ciągu 72 godzin wyjaśnia, kiedy ten termin zaczyna biec.
Czy samo przywrócenie kopii zapasowej wystarczy?
Tylko jeśli kopia jest starsza niż pierwszy backdoor i zamknięto punkt wejścia. W przeciwnym razie razem ze stroną przywraca się problem.
Czy zmiana wszystkich haseł zatrzyma ataki?
Nie. Ocalały backdoor nie potrzebuje hasła. Dane dostępowe wymienia się po poszukiwaniach, a nie zamiast nich.
Dieser Artikel ist eine allgemeine Information und keine Rechtsberatung.
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