Był wieczór, siedziałem nad bazą danych zainfekowanej strony klienta i przeglądałem listę największych wpisów w tabeli ustawień WordPressa. Cztery z nich nazywały się prawie identycznie: „_site_transient_health_” i ciąg znaków szesnastkowych. Każdy miał ponad 120 kilobajtów. Odhaczyłem je jako systemowe, bo WordPress naprawdę trzyma tam takie wpisy, i przeszedłem dalej. To był błąd, który mógł kosztować klienta drugie włamanie.
Tego samego wieczoru, czytając plik konfiguracyjny strony, trafiłem na kilka linijek oznaczonych jako „WP_Core_Integrity”. Robiły jedną rzecz: sprawdzały, czy na dysku jest pewien plik, a jeśli go nie było, sięgały do bazy, wyciągały jeden z tych czterech „systemowych” wpisów, dekodowały go i zapisywały plik z powrotem. Każde wejście na stronę wskrzeszało backdoora. Prowadzę agencję AI i automatyzacji dla MŚP, opiekujemy się kilkudziesięcioma stronami klientów, i to był pierwszy raz, kiedy zobaczyłem tak starannie zbudowaną infekcję na stronie zwykłej małej firmy. Rozbieram ją tu na części, bo ta anatomia tłumaczy, dlaczego „usunąłem wirusa” tak często kończy się nawrotem.
Spis treści
- Dlaczego usunięcie zainfekowanych plików nic nie dało?
- Gdzie leżały 133 pliki i czemu akurat tam?
- Jak kod złośliwy schował się w bazie danych?
- Kilka linijek w pliku konfiguracyjnym, które wskrzeszały wszystko
- Kim było sześciu administratorów, których nikt nie zatrudnił?
- Czy ktoś celował w tę konkretną firmę?
- Ile miejsc trzeba wyczyścić jednocześnie?
- Gdzie się pomyliłem
- Co sprawdzić w swoim WordPressie
- Słownik pojęć
Dlaczego usunięcie zainfekowanych plików nic nie dało?
Pierwszego dnia zrobiliśmy to, co robi większość ludzi po wykryciu włamania. Strona serwowała spam robotom Google (o samym mechanizmie i o tym, jak to wykryliśmy, pisałem w tekście o stronie, która przez miesiąc pokazywała Google coś innego niż ludziom). Odcięliśmy ruch, usunęliśmy podejrzane pliki z głównego katalogu strony, zdjęliśmy plik reguł, który atakujący ustawił tylko do odczytu. Spam przestał lecieć. Wyglądało na załatwione.
Nie było załatwione, bo infekcja nie była jednym obiektem, tylko systemem. Miała cztery niezależne warstwy i każda potrafiła odbudować pozostałe:
- pliki - 133 sztuki rozrzucone po ośmiu katalogach strony
- baza danych - cztery kopie kodu złośliwego zapisane jako wpisy ustawień
- mechanizm odtwarzania - kilka linijek w pliku konfiguracyjnym, które przy każdym żądaniu sprawdzały, czego brakuje, i dorabiały to z bazy
- konta - sześciu fałszywych administratorów, którzy w razie czego mogli po prostu zalogować się i wgrać wszystko od nowa
Najlepiej tłumaczy to analogia z ogrodu. Usunięcie plików to skoszenie chwastów. Trawnik przez tydzień wygląda czysto, ale korzenie siedzą w ziemi, a korzeniami była tu baza danych i plik konfiguracyjny. Do tego ktoś zostawił sobie klucz do furtki, czyli konta administratorów. Możesz kosić co tydzień do końca świata.
Ludzie, którzy pierwszy raz mierzą się z włamaniem, zakładają zwykle, że wirus to „zły plik”. Znajdź, usuń, gotowe. Tak było może dziesięć lat temu. Dzisiejsze infekcje WordPressa są projektowane z myślą o tym, że ktoś będzie je sprzątał. Każda warstwa jest polisą ubezpieczeniową na wypadek utraty pozostałych.
Gdzie leżały 133 pliki i czemu akurat tam?
Liczba 133 robi wrażenie, ale ciekawsze jest to, gdzie te pliki siedziały. Rozkład wyglądał tak: 39 w katalogu wtyczek obowiązkowych, 54 w katalogu na zdjęcia i załączniki, 12 w fałszywej wtyczce, 11 w fałszywym motywie graficznym, reszta drobnicą w kilku innych miejscach. Każde z tych miejsc było wybrane celowo.
Wtyczki, których nie widać w panelu
WordPress ma katalog „mu-plugins”, od „must use”. Wszystko, co tam leży, ładuje się automatycznie przy każdym wyświetleniu strony. Takie pliki nie pojawiają się na zwykłej liście wtyczek w panelu i nie da się ich stamtąd wyłączyć. Dla właściciela strony to katalog praktycznie niewidzialny. Na zdrowej stronie, którą mieliśmy w kopii zapasowej sprzed włamania, leżał tam jeden plik. Na zainfekowanej - czterdzieści.
Nazwy brzmiały jak coś, czego nie chcesz ruszać: „wp-cache-handler”, „wp-core-update”, „wp-db-repair”, „wp-rest-hardening”, „wp-health-check”, każdy z ośmioznakowym dopiskiem. Gdybyś zajrzał tam bez przygotowania, pomyślałbyś, że to elementy samego WordPressa albo czyjaś łatka bezpieczeństwa. „Wzmacnianie zabezpieczeń REST”. Kto by to usunął?
Kod tam, gdzie powinny być tylko zdjęcia
Katalog na przesłane pliki służy do przechowywania obrazków, PDF-ów, cenników. Plik PHP, czyli kod wykonywalny, nie ma tam czego szukać, nigdy. Mimo to leżały tam 54 takie pliki. To ulubione miejsce atakujących z prostego powodu: ten katalog jest zapisywalny przez samą stronę (musi być, inaczej nie wgrasz zdjęcia) i rzadko ktoś go przegląda, bo kto ogląda tysiące miniaturek?
Fałszywa wtyczka i fałszywy motyw
Wtyczka miała nazwę w stylu „media optimization core” z losowym dopiskiem. Dwanaście plików po mniej więcej 100 KB, nazwanych tak, jakby były częścią rdzenia WordPressa. Motyw podobnie. Tu atakujący liczył na znany odruch: na liście wtyczek każdej strony jest kilka pozycji, których właściciel nie pamięta, bo instalował je ktoś trzy lata temu. Jedna więcej nie zwraca uwagi.
Zamek od środka
Do tego dochodził plik reguł serwera z pierwszego dnia, ustawiony tylko do odczytu. Blokował wykonywanie wszystkich plików PHP na stronie poza krótką listą wybraną przez atakującego, z nazwami typu „wp-l0gin” (zero zamiast litery „o”). Dlatego panel logowania zwracał błąd. Panel nie był zepsuty, był zamknięty na klucz, który miał tylko włamywacz. Przy okazji ten zamek chronił infekcję przed konkurencją, czyli przed innymi włamywaczami, i przed skanerami, które próbowałyby coś na stronie uruchomić.
Jak kod złośliwy schował się w bazie danych?
Tu wracam do scenki z początku. WordPress trzyma w tabeli ustawień tzw. transienty, czyli tymczasowe zapiski, które sam tworzy i sam kasuje. Część z nich ma w nazwie „site_transient” i coś o zdrowiu strony. Na każdej instalacji jest ich trochę i nikt na nie nie patrzy.
Atakujący zapisał w tej tabeli cztery wpisy nazwane dokładnie według tego wzorca. W środku każdego siedziało 125 - 141 KB tekstu zakodowanego w base64, czyli zapisanego tak, żeby wyglądał jak losowa sieczka liter. Po zdekodowaniu wychodził pełny kod PHP, zaczynający się od wyłączenia raportowania błędów, żeby nic nie zostawiało śladów w logach.
Cztery kopie, nie jedna. Gdyby ktoś znalazł i usunął jedną, zostają trzy.
Dlaczego sam to przeoczyłem
Patrzyłem na listę największych wpisów, bo to rozsądna metoda: kod złośliwy jest duży, więc szuka się dużych rzeczy. Znalazłem je. I odhaczyłem, bo nazwa pasowała do czegoś, co znam. To jest dokładnie ten sam mechanizm, który opisuję klientom przy modelach językowych: model ocenia dokument po tym, jak wygląda, a nie po tym, co w nim jest, i wtedy z pełnym przekonaniem cytuje coś nieaktualnego. Ja zrobiłem to samo z wpisem w bazie. Zaufałem etykiecie.
Rozstrzyga dopiero zdekodowanie zawartości i sprawdzenie, czy w środku jest kod. Od tamtego wieczoru przy każdym przeglądzie WordPressa dekodujemy duże wpisy w ustawieniach, niezależnie od nazwy. Nazwa wpisu w bazie nie jest dowodem jego niewinności - to zdanie wisi teraz w naszych procedurach.
Kilka linijek w pliku konfiguracyjnym, które wskrzeszały wszystko
Plik konfiguracyjny WordPressa ładuje się przy każdym żądaniu, zanim cokolwiek innego się wydarzy. Są w nim dane do bazy, klucze, kilka ustawień. Mało kto go otwiera po instalacji, bo „tam się nie grzebie”.
W linii 72 siedział blok opisany komentarzem „WP_Core_Integrity”, czyli „integralność rdzenia”. Brzmi jak coś, co chroni stronę. Robił tyle: sprawdzał, czy na dysku jest jeden konkretny plik backdoora. Jeśli był, nie robił nic. Jeśli go nie było, łączył się z bazą, pobierał jeden z czterech zakodowanych wpisów, dekodował go i zapisywał plik z powrotem na dysk.
Zobacz, co to oznacza w praktyce. Sprzątasz pliki, jesteś zadowolony, odświeżasz stronę, żeby sprawdzić, czy działa. I właśnie tym odświeżeniem przywracasz backdoora. Najgorsze w tym mechanizmie jest to, że sprzątający sam go uruchamia, w momencie kiedy chce się upewnić, że posprzątał.
Używam czasem porównania do agentów AI z pamięcią. Agent, który ma trwałą pamięć poza rozmową, po restarcie wraca do tego samego stanu, bo czyta swoje notatki. Możesz mu wyczyścić bieżącą sesję, ale dopóki notatki leżą w bazie, wróci taki sam. Tu było identycznie, tylko notatki pisał włamywacz, a „restartem” było każde wejście na stronę.
Kim było sześciu administratorów, których nikt nie zatrudnił?
Prawdziwe konta administratorów na tej stronie były trzy, wszystkie założone w 2024 roku. Fałszywych było sześć, dopisywanych od 20 lipca do 7 sierpnia. Nazwy wyglądały na techniczne: prefiksy w rodzaju „wpsvc_” czy „wp2_”, kilka udawało nazwę firmy z dopiskiem „dev” albo „web”. Adresy e-mail w domenach, które nie istnieją, na przykład z końcówką „.invalid” albo „.internal”. Takiej domeny nie da się zarejestrować, więc nikt nigdy nie dostanie na nią maila. To sygnatura automatu, nie człowieka.
Po co atakującemu konta, skoro ma pliki, bazę i mechanizm odtwarzania? Bo konto administratora jest najprostszą i najbardziej legalnie wyglądającą drogą powrotu. Jeśli ktoś wyczyści wszystko inne, włamywacz loguje się jak każdy pracownik i wgrywa wtyczkę. Żaden skaner nie podniesie alarmu, bo administrator instalujący wtyczkę to normalna czynność.
Daty kont dały nam też chronologię, której nie dały daty plików. Plik reguł podrzucony przez atakującego miał datę cofniętą o rok, co jest standardową sztuczką zacierania śladów (pisałem o tym osobno w tekście o tym, że daty plików kłamią). Konta w bazie miały daty prawdziwe, a reszty dopełniła kopia zapasowa. Wyszło z nich, że pierwszy plik pojawił się 3 lipca, pierwsze konta 20 i 21 lipca, kolejne 26 lipca, a ostatnie pliki i konta 6 i 7 sierpnia. Atakujący był aktywny do samego końca, praktycznie do dnia, w którym go zauważyliśmy.
Jedno zdanie o tym, jak to wygląda u typowej małej firmy. Lista administratorów to rzecz, której nikt nie przegląda, bo „przecież wiemy, kto ma dostęp”. A potem okazuje się, że na stronie jest konto po agencji sprzed pięciu lat, konto byłego pracownika, konto „test” i dwa, których nikt nie rozpoznaje. W takim tłumie szósty fałszywy admin jest niewidzialny.
Czy ktoś celował w tę konkretną firmę?
Nie. I to jest mit, który chcę rozbroić najmocniej, bo słyszę go od klientów co tydzień: „kto by się włamywał do nas, jesteśmy małą firmą”.
Te same prefiksy w nazwach kont i te same nieistniejące domeny w adresach e-mail znaleźliśmy później na dwóch kolejnych stronach. Jedno narzędzie, trzy strony, trzech różnych właścicieli, którzy nic o sobie nie wiedzą. Sprawdziliśmy też, czy strony nie zaraziły się od siebie nawzajem. Nie mogły: każda ma osobnego użytkownika systemowego, osobną bazę i osobne konto do bazy, a konto jednej strony po zalogowaniu widzi wyłącznie swoją bazę. Czyli każda z trzech została zajęta osobno, najpewniej tą samą podatnością albo tym samym rodzajem słabego hasła.
Tak wygląda dzisiejszy atak na stronę małej firmy. Nikt nie siedzi w kapturze nad twoją ofertą. Automat skanuje internet w poszukiwaniu konkretnej słabości, a kiedy ją znajdzie, instaluje cały pakiet: pliki, wpisy w bazie, mechanizm odtwarzania i konta. Mała firma nie jest celem. Mała firma jest zasobem - serwerem z dobrą reputacją w Google, z którego można rozsyłać spam i na którym nikt nie patrzy.
Jest w tym gorzka ironia. Większość tego, co piszę na tym blogu, dotyczy automatyzacji, która odciąża firmę. Tu po drugiej stronie stała automatyzacja zrobiona porządnie, z redundancją, z kamuflażem, z planem na sprzątanie. Lepiej przemyślana niż niejeden system, który widziałem w firmach. Szczerze? Zrobiło mi się zimno, kiedy to zrozumiałem.
Cudza infekcja przyciąga zresztą kolejnych. W logach z dnia odcięcia widzieliśmy skanowanie z chmury publicznej, które sprawdzało kilkanaście ścieżek typowych dla backdoorów. Ktoś inny szukał drzwi, które ktoś przed nim zostawił otwarte.
Ile miejsc trzeba wyczyścić jednocześnie?
Wszystkie. Jednocześnie. W naszym przypadku oznaczało to pięć obszarów w jednym oknie czasowym:
- wszystkie pliki z ośmiu lokalizacji, łącznie z katalogiem wtyczek obowiązkowych i plikami PHP w katalogu na zdjęcia
- cztery wpisy w bazie danych
- blok w pliku konfiguracyjnym
- plik reguł serwera
- sześć fałszywych kont administratora
Pominięcie którejkolwiek warstwy oznacza powrót. Jeśli usuniesz pliki, a zostawisz bazę - plik konfiguracyjny je odtworzy. Jeśli wyczyścisz plik konfiguracyjny, a zostawisz konta - włamywacz wejdzie drzwiami frontowymi. Jeśli zrobisz wszystko poza jednym plikiem w katalogu zdjęć - masz otwartą furtkę, o której nie wiesz.
Dlaczego rekomendowałem odtworzenie, a nie sprzątanie
133 pliki w ośmiu miejscach to dla mnie pewne przeoczenie przy ręcznym sprzątaniu. Nie „możliwe”, tylko pewne. Dlatego zarekomendowałem odtworzenie strony z kopii zapasowej sprzed włamania. Mieliśmy punkt z 2 lipca, czyli dzień przed pierwszym plikiem atakującego, i sprawdziliśmy go odczytem, nie deklaracją: wyciągnęliśmy z niego plik reguł (oryginalny, 884 bajty, zamiast 533 bajtów wersji atakującego) i zajrzeliśmy do katalogu wtyczek obowiązkowych (jeden plik zamiast czterdziestu). Ta kopia załatwiała 127 ze 133 plików. Jak ważne jest takie sprawdzenie, opisałem w tekście o tym, że backup, którego nikt nie otworzył, nie jest backupem.
Ale i tu jest haczyk, który łatwo przegapić. Odtworzenie samych plików nie wystarczy, bo baza zostaje ta sama, z czterema zakodowanymi kopiami i sześcioma kontami. Trzeba odtworzyć albo przejrzeć także bazę, zmienić wszystkie hasła i klucze, a dopiero potem wpuścić ruch.
I najtrudniejsze: którędy weszli
Odtworzenie strony bez ustalenia drogi wejścia to zaproszenie do powtórki tą samą ścieżką. Tu napotkaliśmy ścianę: serwer trzymał logi ruchu tylko z 10 dni. Pierwszy plik pojawił się 3 lipca, my patrzyliśmy na logi 10 sierpnia. Droga wejścia była poza zasięgiem. Od tamtej pory przy stronach klientów podnosimy retencję logów, zanim będzie potrzebna, bo w dniu włamania jest na to za późno.
Gdzie się pomyliłem
Pierwszego dnia, po odcięciu zainfekowanej strony, przejrzałem pozostałe strony na serwerze i napisałem w raporcie, że zarażona była tylko ta jedna. „1 z 68”. Brzmiało to rzetelnie: przeszukałem wszystkie katalogi, sprawdziłem nazwy plików i daty modyfikacji w głównych folderach stron.
Trzy dni później, przy zupełnie innej okazji (zmienialiśmy hasła do kilku WordPressów), przejrzałem wszystkie instalacje pod jednym kątem: konta administratorów założone w ostatnich 90 dniach. Wyszły dwie kolejne zajęte strony. Jedna miała 21 plików w katalogu wtyczek obowiązkowych i aktywną wtyczkę-backdoora, a jej fałszywe konta istniały już w chwili, gdy pisałem „1 z 68”. Druga, na innym serwerze, dostała fałszywego administratora o pierwszej w nocy 13 sierpnia, czyli trzy dni po naszym sprzątaniu.
Do tego pierwsza strona, ta „wyczyszczona”, wciąż miała 40 plików w katalogu wtyczek obowiązkowych. Usunęliśmy pliki z głównego katalogu i zablokowaliśmy ruch, a farma backdoorów ładowała się dalej. Nawet liczba w raporcie była zła. 68 to były katalogi na serwerze, a instalacji WordPressa było 30.
Błąd nie polegał na tym, że czegoś nie zauważyłem. Polegał na metodzie: szukałem w miejscach, w których szuka się zwykle, i po datach, które atakujący sfałszował. Infekcja siedziała tam, gdzie nie patrzyłem - w katalogu ładowanym automatycznie i w bazie. Sprostowałem raport, ale przez trzy dni zespół i klienci żyli w przekonaniu, że sprawa jest zamknięta. „Sprawdziłem, czysto” to zdanie, na którym ludzie budują decyzje, i od tamtej pory nie mówię go bez sprawdzenia wszystkich warstw.
Ta sama infekcja odezwała się zresztą jeszcze raz. Na początku września na jednej z zajętych stron uruchomił się mechanizm rozsyłania spamu, zanim zdążyliśmy przeprowadzić pełne odtworzenie. Obie najmocniej zajęte strony ostatecznie spakowaliśmy do archiwum, sprawdziliśmy sumy kontrolne kopii i wyłączyliśmy. Nie każda historia kończy się happy endem w postaci „posprzątane, działa”.
Co sprawdzić w swoim WordPressie
Nie potrzebujesz do tego informatyka na pełen etat, ale część punktów wymaga dostępu do serwera, więc podzieliłem je na dwie grupy.
Sam, w panelu, w kwadrans
- Lista użytkowników z rolą administratora. Każde konto musisz umieć przypisać do konkretnej osoby, która dziś z tobą pracuje. Konto, którego nie rozpoznajesz, to nie zapomniany test, dopóki nie udowodnisz, że jest inaczej. Zwróć uwagę na adresy e-mail w dziwnych domenach.
- Lista wtyczek i motywów. Wszystko, czego nie używasz, usuń, nie tylko wyłącz. Nazwy brzmiące jak „core”, „compat”, „optimization” z losowym dopiskiem traktuj podejrzliwie.
- Wynik wyszukiwania site: z adresem twojej strony w Google. Jeśli widzisz podstrony w obcym języku albo o produktach, których nie sprzedajesz, coś jest nie tak.
Do zlecenia osobie, która opiekuje się stroną
- Katalog wtyczek obowiązkowych (mu-plugins). Na większości stron jest pusty albo ma jeden - dwa znane pliki. Każdy plik powinien mieć wyjaśnienie.
- Pliki PHP w katalogu na przesłane pliki. Nie powinno ich tam być wcale. Każdy jeden to sygnał alarmowy.
- Koniec pliku konfiguracyjnego. Wszystko, co wykracza poza standardowe ustawienia, a zwłaszcza kod łączący się z bazą albo zapisujący pliki, wymaga wyjaśnienia.
- Duże wpisy w tabeli ustawień. Zdekodować i sprawdzić zawartość, niezależnie od tego, jak niewinnie brzmi nazwa.
- Retencja logów. Ile dni wstecz serwer pamięta ruch? Jeśli 10, to przy włamaniu sprzed miesiąca nie ustalisz drogi wejścia.
- Kopia zapasowa sprawdzona odczytem. Nie „mamy backup”, tylko „wczoraj odzyskaliśmy z niego plik i porównaliśmy”. I pytanie, jak daleko wstecz sięga, bo infekcja potrafi żyć tygodniami, zanim ją zauważysz.
Jeśli prowadzisz firmę, która korzysta z AI i automatyzacji, dorzuciłbym jeszcze jedno pytanie, szersze niż WordPress: które twoje systemy mają pamięć, która przetrwa sprzątanie? Bazy, pliki konfiguracyjne, konta usługowe, zapisane tokeny. To tam zostają korzenie, kiedy kosisz chwasty.
Słownik pojęć
- Backdoor - ukryte wejście do systemu zostawione przez włamywacza, pozwalające mu wracać bez ponownego łamania zabezpieczeń. Może być plikiem, wpisem w bazie albo kontem użytkownika.
- mu-plugins (wtyczki obowiązkowe) - katalog WordPressa, z którego wszystkie pliki ładują się automatycznie przy każdym wyświetleniu strony. Nie widać ich na zwykłej liście wtyczek i nie da się ich wyłączyć z panelu.
- Transient - tymczasowy wpis, który WordPress sam zapisuje w bazie i sam usuwa po czasie. Ich nazwy są typowe i powtarzalne, dlatego włamywacze chętnie się pod nie podszywają.
- Base64 - sposób zapisu danych za pomocą zwykłych liter i cyfr. Nie jest szyfrowaniem, ale zakodowany kod wygląda jak losowy ciąg znaków i nie zdradza się przy pobieżnym przeglądaniu.
- Plik konfiguracyjny WordPressa (wp-config.php) - plik z danymi do bazy i podstawowymi ustawieniami, ładowany jako jeden z pierwszych przy każdym żądaniu. Kod dopisany na jego końcu wykonuje się zawsze.
- Retencja logów - liczba dni, przez które serwer przechowuje zapis ruchu. Od niej zależy, czy po włamaniu da się ustalić, którędy wszedł atakujący.
Twój WordPress ma więcej administratorów, niż myślisz?
Zrób dziś jedną rzecz: otwórz listę użytkowników w panelu i policz administratorów. Jeśli któregoś nie umiesz przypisać do konkretnej osoby albo w katalogu wtyczek obowiązkowych leży coś, czego nikt nie potrafi wyjaśnić, napisz do mnie na [email protected]. Najpierw powiem ci, czego nie ruszać, żeby nie zniszczyć śladów i nie uruchomić przy okazji mechanizmu odtwarzania. Dopiero potem, od czego zacząć.
własnej firmie cybulski.ai JC

