Jak radzić sobie z wirusami na WordPressie — praktyczny poradnik po włamaniu

Strona działa wolniej niż zwykle. Klient pisze, że Google pokazuje dziwne wyniki. Albo — gorzej — logujesz się do panelu i widzisz konto admina, którego nie tworzyłeś.

Brzmi znajomo?

Jeśli prowadzisz stronę na WordPressie i coś Ci tu nie gra — ten wpis jest dla Ciebie. Bez paniki, bez marketingowej piany. Konkretna instrukcja: co sprawdzić, jak oczyścić, czego szukać w miejscach, o których nie pisze się w żadnym poradniku.

A na koniec pokażę Ci prawdziwe case study z naszej praktyki — backdoor, którego nie znajdzie żaden skaner.

1. Jak rozpoznać, że strona jest zainfekowana

Nie zawsze wirusy na WordPressie objawiają się spektakularnie. Część infekcji działa po cichu — miesiącami. Oto sygnały, na które warto zwrócić uwagę:

Objawy widoczne gołym okiem

  • Przekierowania na obce strony — wchodzisz na swoją stronę, a przeglądarka ląduje na kasynie, farmacji albo stronie ze scamem. Czasem tylko z mobile, czasem tylko z Google (nie z bezpośredniego wejścia).
  • Dziwne podstrony w Google — szukasz swojej firmy, a w wynikach pojawiają się strony z chińskimi znakami, kasynami, viagrą. To klasyczny SEO spam (Japanese Keyword Hack).
  • Ostrzeżenie przeglądarki — Chrome wyświetla czerwony ekran „Niebezpieczna strona”. Na tym etapie Google już Cię oflagował.
  • Nowe konta użytkowników — w panelu WP widzisz admina, którego nie tworzyłeś. Login typu „devuser”, „wp_service”, „admin0″.
  • Zmodyfikowane pliki — functions.php, wp-config.php albo .htaccess mają kod, którego nie wstawiałeś.

Objawy mniej oczywiste

  • Strona wolniejsza niż zwykle — złośliwy kod obciąża serwer, szczególnie skrypty kopiące kryptowaluty albo rozsyłające spam mailowy.
  • Problemy z wysyłką maili — Twoje maile lądują w spamie? Możliwe, że ktoś rozsyła spam z Twojego serwera i IP jest na czarnej liście.
  • Dziwne wpisy w Google Search Console — nagły skok zaindeksowanych stron (z 50 do 3000), ostrzeżenia o malware, pokrycie indeksu z URL-ami, które nie są Twoje.
  • Wzrost ruchu z dziwnych krajów — nagle masz 5000 wizyt z Wietnamu albo Indonezji? Sprawdź co odwiedzają.

Jeśli zauważyłeś chociaż jeden z tych punktów — nie czekaj. Działaj teraz.

2. Pierwsza pomoc — co zrobić natychmiast

Zanim zaczniesz grzebać w plikach, zabezpiecz podstawy. To jest Twoja lista „zanim cokolwiek ruszysz”:

Krok 1: Zrób backup

Tak — backup zainfekowanej strony. Brzmi absurdalnie, ale potrzebujesz kopii stanu „przed czyszczeniem”. Jeśli coś popsujesz przy naprawie, chcesz mieć do czego wrócić. Poza tym — pliki z backdoorami to dowody, które pomogą zrozumieć jak doszło do włamania.

Krok 2: Zmień wszystkie hasła

I to nie tylko hasło do panelu WordPress. Zmień:

  • Hasła wszystkich użytkowników WordPress (szczególnie adminów)
  • Hasło do bazy danych (w panelu hostingu, potem zaktualizuj wp-config.php)
  • Hasło do panelu hostingu (cPanel, DirectAdmin, panel dostawcy)
  • Hasło do FTP/SFTP
  • Hasło do konta email powiązanego z WP

Krok 3: Zregeneruj klucze bezpieczeństwa

W pliku wp-config.php masz sekcję z solami (AUTH_KEY, SECURE_AUTH_KEY itd.). Wygeneruj nowe na stronie api.wordpress.org/secret-key/1.1/salt/ i wklej w miejsce starych. To wyloguje wszystkie aktywne sesje — w tym sesję atakującego.

Krok 4: Wyłącz edycję plików z panelu

Dodaj do wp-config.php dwie stałe: DISALLOW_FILE_EDIT (blokuje Theme Editor i Plugin Editor) oraz DISALLOW_FILE_MODS (blokuje też instalację wtyczek i motywów z panelu). Atakujący z dostępem do panelu WP nie będzie mógł wrzucić kodu przez edytor.

Krok 5: Sprawdź konta administratorów

Wejdź w Użytkownicy, filtruj po roli Administrator. Każde konto, którego nie rozpoznajesz — usuń. Sprawdź też datę rejestracji — atakujący lubią backdatować konta (ustawiać datę rejestracji na kilka lat wstecz), żeby wyglądały na stare i legalne.

3. Gdzie szukać malware — pliki, baza danych, cron, .htaccess

OK, podstawy zabezpieczone. Teraz trzeba znaleźć złośliwy kod. Oto mapa — gdzie szukać i na co patrzeć.

Pliki PHP

To najczęstszy wektor. Sprawdź:

  • functions.php każdego zainstalowanego motywu (aktywnego i nieaktywnych — atakujący uwielbiają chować się w nieaktywnych motywach)
  • wp-content/uploads/ — nie powinno być tu żadnych plików PHP. Jeśli jest — to backdoor
  • wp-content/mu-plugins/ — Must-Use plugins ładują się automatycznie, nie widać ich na liście wtyczek w panelu
  • Nieznane pluginy — foldery w wp-content/plugins/ z nazwami typu „wp-promtools-pro”, „PHP-Console”, „hello-developer”. Nie znasz? Usuń
  • wp-config.php — sprawdź czy nie ma dopisanego kodu na początku lub końcu pliku
  • index.php w katalogu głównym — powinien mieć 2-3 linijki. Jeśli ma więcej — coś jest nie tak

Szybki sposób na znalezienie podejrzanych plików — w terminalu szukaj plików PHP zmodyfikowanych w ostatnich 30 dniach w katalogu wp-content. To pokaże co się ostatnio zmieniało.

.htaccess

Plik .htaccess w katalogu głównym to częste miejsce na przekierowania. Powinien wyglądać prosto — blok BEGIN WordPress / END WordPress i nic więcej. Jeśli widzisz zakodowane linijki z RewriteRule prowadzące na obce domeny — to malware.

Sprawdź też .htaccess w wp-content/ i wp-includes/. Domyślnie ich tam nie ma.

Baza danych — wp_options

Tabela wp_options to skarbnica dla atakujących. Szukaj opcji, których wartości zawierają podejrzane funkcje PHP — takie jak base64_decode(), system() albo inne wywołania, które nie powinny siedzieć w opcjach WordPressa.

Sprawdź też:

  • Transients — tymczasowe opcje, które mogą zawierać złośliwy kod
  • Opcje z autoload — ładują się przy każdym otwarciu strony

WP-Cron i Action Scheduler

Złośliwy kod może być zaplanowany do odpalenia w przyszłości. Sprawdź listę zaplanowanych eventów WP-Cron (przez WP-CLI: wp cron event list). Szukaj eventów, których nie rozpoznajesz. Szczególnie z nazwami typu „wp_system_update”, „wp_service_check” — wyglądają na legalne, ale nie są częścią WordPressa.

Jeśli masz WooCommerce, sprawdź też Action Scheduler (WooCommerce, Status, Zaplanowane akcje). Atakujący potrafią tam ukryć zadania.

System crontab

Na serwerze sprawdź crontab -l i /etc/crontab. Złośliwy cron na poziomie systemu przeżyje każde czyszczenie WordPressa.

4. Case study: Backdoor w MySQL TRIGGER — prawdziwa historia

To jest część, która odróżnia ten poradnik od każdego innego artykułu o wirusach na WordPressie. Bo to, co tu opisuję, nie jest teorią. To sytuacja z naszej praktyki — i rozwiązanie, które wymyśliliśmy po wyczerpaniu wszystkich standardowych metod.

Sytuacja wyjściowa

Klient z branży e-commerce. Sklep na WooCommerce, kilkaset zamówień miesięcznie. Po włamaniu (brute-force na xmlrpc, złamane hasło, backdoor w plikach) oczyściliśmy wszystko:

  • Pliki PHP — functions.php, mu-plugins, uploads, każdy folder
  • Bazę danych — wp_options, wp_cron, snippety, Action Scheduler
  • Hasła — WordPress, baza, hosting, FTP
  • Salt keys — zregenerowane
  • Sesje — wylogowanie wszystkich
  • WooCommerce REST API keys — zregenerowane
  • DISALLOW_FILE_MODS — włączone

Standard. Zrobiliśmy to, co robi każda dobra firma od bezpieczeństwa.

Problem: konto admin wraca

Po oczyszczeniu pojawiło się konto administratora „devuser”. Usunęliśmy je. Wróciło. Usunęliśmy znowu. Wróciło znowu.

Data rejestracji konta? 2021 rok. Trzy lata wstecz. Sprytne — na liście użytkowników wyglądało na stare konto deweloperskie.

Systematyczna eliminacja

Przeszukaliśmy każdy możliwy wektor:

  • functions.php — wielokrotne odświeżenie strony po usunięciu konta, brak nowych kont
  • wp_cron — czysto
  • wp_options (autoload, transients) — czysto
  • mu-plugins — tylko Elementor safe mode
  • drop-ins — brak
  • Action Scheduler WooCommerce — czysto
  • WooCommerce webhooks — brak
  • uploads/ — brak PHP
  • .htaccess — czysty
  • auto_prepend_file w PHP — nie ustawiony
  • system crontab — pusty
  • grep po plikach za „devuser” — zero wyników

Nic. Żaden plik na serwerze nie zawierał słowa „devuser”. Żaden cron nie tworzył konta. Żaden skaner — Wordfence, Sucuri, WP-CLI — nic nie znajdował.

A konto wracało.

Rozwiązanie: SHOW TRIGGERS

Po wyczerpaniu wszystkich standardowych metod sprawdziliśmy coś, czego nie robi żaden popularny skaner WordPressa:

SHOW TRIGGERS;

I tam był.

MySQL TRIGGER na tabeli wp_comments. Nazywał się after_insert_comment. Przy każdym nowym komentarzu, który zawierał konkretną frazę spamową, trigger automatycznie tworzył konto administratora z backdatowaną datą rejestracji (rok 2021) i znanym hasłem.

Mechanizm: jeśli treść komentarza zawiera określoną frazę, trigger tworzy nowego użytkownika „devuser” z rolą administrator i ustawia datę rejestracji na 2021, żeby wyglądał na stare konto.

Prosty. Elegancki. I totalnie niewidzialny.

Dlaczego to było niewidoczne

To kluczowa rzecz do zrozumienia. MySQL trigger działa na poziomie silnika bazy danych. Nie jest plikiem PHP. Nie jest opcją w wp_options. Nie jest cronem. Istnieje tylko w strukturze bazy danych — w INFORMATION_SCHEMA.TRIGGERS.

Dlatego:

  • Wordfence go nie widzi — skanuje pliki PHP, nie strukturę bazy
  • Sucuri go nie widzi — to samo
  • WP-CLI go nie widzi — operuje na warstwie PHP/WordPress
  • Grep po plikach go nie znajdzie — bo nie istnieje w żadnym pliku
  • DISALLOW_FILE_MODS go nie blokuje — nie modyfikuje plików
  • Zmiana haseł nie pomaga — trigger tworzy NOWE konto, nie loguje się na stare
  • Nie zostawia śladów w logach HTTP — bo nie generuje żadnych zapytań HTTP

Dlaczego trigger się odpalał mimo wyłączonych komentarzy

Tu jest dodatkowy haczyk. Klient wyłączył komentarze w ustawieniach WordPressa. Ale trigger nadal działał. Dlaczego?

WooCommerce. Notatki zamówień (order notes) są zapisywane w tabeli wp_comments z typem „order_note”. Każda zmiana statusu zamówienia = INSERT do wp_comments = trigger się odpala.

Poza tym — spam boty POSTują bezpośrednio na wp-comments-post.php niezależnie od ustawień Dyskusji. WordPress robi INSERT do bazy nawet dla komentarzy oznaczonych jako spam.

Wystarczył jeden spamowy komentarz z odpowiednią frazą — i konto admin było z powrotem.

Jak to naprawiliśmy

Trzy komendy SQL: usunięcie triggera (DROP TRIGGER), usunięcie kont „devuser” z tabeli wp_users i wp_usermeta. A potem weryfikacja — SHOW TRIGGERS (powinno być puste), SHOW EVENTS, SHOW PROCEDURE STATUS i SHOW FUNCTION STATUS — żeby upewnić się, że nie ma innych wektorów bazodanowych.

Po usunięciu triggera — konto admin przestało wracać. Problem rozwiązany.

Lekcja

WordPress natywnie NIE używa triggerów MySQL. Jakikolwiek trigger w bazie WordPressa to czerwona flaga. Jeśli go widzisz — ktoś go tam celowo umieścił.

To był backdoor, o którym nie pisze się w żadnym poradniku bezpieczeństwa. Teraz już wiesz, że istnieje.

5. Checklista po włamaniu — 15 punktów

Wydrukuj to albo zapisz w zakładkach. Kiedy łapiesz wirusa na WordPressie, przejdź przez tę listę punkt po punkcie:

  1. Backup zainfekowanej strony — przed czyszczeniem, jako dowód i punkt powrotu
  2. Zmiana wszystkich haseł — WordPress, baza, hosting, FTP, email
  3. Regeneracja salt keys w wp-config.php
  4. Usunięcie nieznanych kont admin — sprawdź daty rejestracji (backdating!)
  5. Skan plików PHP — functions.php (wszystkich motywów), mu-plugins, uploads, index.php
  6. Sprawdzenie .htaccess — w katalogu głównym, wp-content i wp-includes
  7. Audyt wp_options — szukaj podejrzanych funkcji PHP w wartościach opcji
  8. Czyszczenie WP-Cron — sprawdź zaplanowane eventy, usuń nieznane
  9. Czyszczenie Action Scheduler — jeśli masz WooCommerce
  10. Sprawdzenie system crontab — crontab na serwerze
  11. SHOW TRIGGERS — sprawdź triggery MySQL. Jakikolwiek trigger = czerwona flaga
  12. SHOW EVENTS — MySQL events mogą być kolejnym wektorem
  13. SHOW PROCEDURE STATUS / SHOW FUNCTION STATUS — procedury i funkcje w bazie
  14. DISALLOW_FILE_EDIT + DISALLOW_FILE_MODS — zablokuj edycję i instalację z panelu
  15. Weryfikacja integralności core WP — wp core verify-checksums przez WP-CLI

Punkty 11-13 to jest to, czego nie ma w żadnej standardowej checkliście. A jak widzisz z naszego case study — mogą być jedynym sposobem na znalezienie backdoora.

6. Jak się zabezpieczyć na przyszłość

Czyszczenie po włamaniu to gaszenie pożaru. Lepiej nie dopuścić do ognia. Oto co naprawdę chroni stronę na WordPressie:

Zablokuj xmlrpc.php

To jest wektor nr 1 dla brute-force. Zablokuj plik xmlrpc.php w .htaccess (Require all denied) albo na poziomie Cloudflare/serwera. Jeśli nie korzystasz z aplikacji mobilnej WordPressa ani z Jetpacka — nie potrzebujesz xmlrpc.

Zablokuj enumerację użytkowników

Endpoint /wp-json/wp/v2/users domyślnie jest publiczny. Zablokuj go — albo wtyczką (np. Disable REST API), albo filtrem w functions.php, który usuwa ten endpoint dla niezalogowanych.

Włącz 2FA

Dwuskładnikowe uwierzytelnianie na kontach administratorów. Nawet jeśli atakujący złamie hasło — bez kodu z telefonu się nie zaloguje. Fluent Auth, WP 2FA, albo Wordfence — cokolwiek, ale włącz.

Silne hasła + limit prób logowania

Hasła minimum 16 znaków, losowe. Limit prób logowania — 3-5 prób, potem blokada IP na 30 minut. Fail2Ban na serwerze to jeszcze lepsze rozwiązanie.

Aktualizacje

Banał, ale statystyki są brutalne — większość włamań wykorzystuje znane luki w nieaktualnych pluginach. Aktualizuj core, pluginy i motywy. Regularnie. Nie „jak będę miał czas”.

Monitoring

Nie czekaj aż klient Ci powie, że Google pokazuje kasyna. Ustaw:

  • Google Search Console — alerty o problemach z bezpieczeństwem i nagłych zmianach w indeksie
  • Monitoring uprawnień — codzienne sprawdzanie listy adminów (automatycznie, np. skryptem WP-CLI)
  • Monitoring plików — powiadomienie gdy zmieni się functions.php albo wp-config.php

Potrzebujesz pomocy?

Podejrzewasz, że Twoja strona jest zainfekowana? Albo czyścisz po włamaniu i konto admin wraca jak bumerang?

Napisz do mnie. Przejdę przez Twoją stronę, sprawdzę to, czego nie sprawdzają skanery — łącznie z MySQL triggers — i pomogę przywrócić porządek.

A jeśli wolisz spokój głowy na stałe — mamy Growth Partner: comiesięczna opieka nad stroną z monitoringiem bezpieczeństwa, aktualizacjami i reakcją na incydenty. Żebyś nie musiał się budzić w nocy z myślą „a co jeśli ktoś mi właśnie hacknął sklep”.

hello@weblymate.com — odpisuję tego samego dnia.