Klient napisał do nas w jednej, prostej sprawie: strona zacina się przy przewijaniu, a czasem zawiesza na dobre. Zlecenie brzmiało „napraw scroll”. Zanim jednak zajrzeliśmy w konfigurację, zrobiliśmy to, co robimy zawsze na początku, czyli zeszliśmy z produkcji na kopię. Pobraliśmy pełny zrzut plików i bazy, żeby pracować na swoim, a nie na żywej witrynie odwiedzanej przez ludzi. Pierwszy listing katalogu głównego wystarczył, żeby rozmowa o płynności przewijania musiała poczekać. Strona była przejęta, i to nie powierzchownie. Poniżej opisujemy, co znaleźliśmy: wielowarstwową infekcję WordPressa, w której backdoory udawały pliki rdzenia, baza sama odtwarzała skasowany kod, a zwykły komentarz pod wpisem potrafił przywrócić konto administratora. Pokazujemy też, jak rozłożyliśmy to na części i wyczyściliśmy, nie tracąc ani jednej strony treści klienta. Nazwę klienta i domenę pomijamy; nazwy samych plików malware zostawiamy, bo bez nich ta historia niczego nie uczy.
Zaczęło się od zgłoszenia „napraw scroll”
Zgłoszenie było konkretne i przyziemne. Strona wydarzenia, zbudowana na WordPressie z Elementorem i postawiona na hostingu współdzielonym, zacinała się przy przewijaniu, a na słabszym sprzęcie potrafiła zawiesić kartę przeglądarki. Nic w tym opisie nie zapowiadało włamania. Nasza diagnostyka wydajności zaczyna się jednak zawsze tak samo, od zejścia z produkcji: pobieramy pełną kopię witryny i zrzut bazy, stawiamy je lokalnie i dopiero tam szukamy przyczyny, żeby nie eksperymentować na żywym ruchu.
Już pierwszy listing katalogu głównego pokazał pliki, których w czystej instalacji WordPressa być nie może. Leżał tam loader uruchamiający kod rozpakowany z archiwum ZIP, osobny skrypt logujący odwiedzającego jako pierwszy administrator w bazie i kilka „resetów cache”, które z buforowaniem nie miały nic wspólnego. W tym momencie zlecenie przestało dotyczyć przewijania. Odezwaliśmy się do klienta, wyjaśniliśmy, na co patrzymy, i za jego wyraźną zgodą przeszliśmy od optymalizacji do reagowania na incydent.
Trzy warstwy, z których każda odbudowuje pozostałe
To nie był pojedynczy webshell wrzucony przez przypadkową dziurę. Infekcja była pomyślana tak, żeby przetrwać połowiczne sprzątanie. Składała się z trzech warstw, a każda z nich potrafiła odtworzyć dwie pozostałe. Dopóki usuwało się je po kolei, witryna wracała do stanu sprzed czyszczenia w ciągu jednego załadowania strony. W liczbach wyglądało to tak:
| Co znaleźliśmy | Liczba |
|---|---|
| Pliki malware (webshelle, podstawione wtyczki i motywy) | ponad 30 |
| Backdoory podszyte pod pliki rdzenia WordPressa | 12 |
| Fałszywe konta administratora z aktywną sesją | 4 |
| Wyzwalacze SQL odtwarzające administratora po usunięciu | 1 |
Warstwa pierwsza była widoczna gołym okiem. W katalogu głównym siedziały webshelle, a obok szesnaście podstawionych wtyczek i motywów o nazwach z doklejonym znacznikiem czasu, tak żeby wtapiały się w listę rozszerzeń. Najaktywniejszy element działał jako wtyczka typu must-use, ładowana automatycznie przy każdym żądaniu, bez wpisu w panelu. Pełnił rolę pośrednika w rozmowie z serwerem sterującym i podsuwał odwiedzającym z Windowsa fałszywe okno „weryfikacji Cloudflare”, czyli atak ClickFix, który namawia użytkownika, żeby własnoręcznie wkleił polecenie do konsoli systemu. Pod tę warstwę podpadały między innymi mu-plugins/vista-relay-x.php, zaciemniony pośrednik o wadze ponad stu kilobajtów, oraz mu-plugins/image-loader.php, który po cichu przekierowywał podstronę kontaktu na obcą domenę.
Warstwa druga mieszkała w bazie danych i była groźniejsza, bo nie zostawiała śladu w plikach. Payload backdoora leżał w tabeli opcji z włączonym autoładowaniem, więc wczytywał się przy każdym otwarciu strony, a obok niego kilka opcji z telemetrią i konfiguracją serwera dowodzenia. Najciekawszy okazał się wyzwalacz MySQL założony na tabeli komentarzy. Wystarczyło dodać komentarz zawierający ustaloną frazę, żeby baza sama, bez udziału jakiegokolwiek pliku PHP, wstawiła nowe konto administratora. Kanał odzyskiwania dostępu leżał więc całkowicie poza tym, co zwykle się przeszukuje.
Warstwa trzecia była ukryta najlepiej. Dwanaście backdoorów podszyło się pod pliki rdzenia WordPressa i rozłożyło w katalogach wp-admin oraz wp-includes pod nazwami, które nie budzą podejrzeń. Skan po sygnaturach ich nie ruszył, bo nazwy i lokalizacje wyglądały wiarygodnie. Ujawniło je dopiero porównanie każdego pliku z oficjalnym wydaniem WordPressa, a więc nie pytanie „czy plik wygląda złośliwie”, tylko „czy ten plik w ogóle powinien tu być”. Wśród nich znalazł się kompletny mechanizm zdalnego wykonania kodu, ukryty w pliku udającym klasę typu wpisu, który uruchamiał dowolny kod pobrany z adresu podanego w jednym parametrze URL.
Dlaczego skasowanie tego, co widać, niczego nie kończy
Najczęstszy błąd przy takim sprzątaniu polega na skasowaniu tego, co widać, i uznaniu sprawy za zamkniętą. Tutaj każdy usunięty element miał swojego dublera w innej warstwie. Loadery z plików odczytywały payload z bazy i odtwarzały skasowane rozszerzenia. Autoładowana opcja przywracała logikę backdoora nawet po wyczyszczeniu plików. Wyzwalacz na komentarzach odbudowywał konto administratora niezależnie od plików i opcji. A gdyby któraś warstwa jednak padła, cyklicznie uruchamiane zadanie dbało o to, żeby dociągnąć z serwera sterującego świeżą wersję payloadu. Pętla domykała się sama.
Nie jest to teoria dopisana na potrzeby opisu. W trakcie analizy usunęliśmy cztery konta administratora, a jedno z nich natychmiast pojawiło się ponownie, tym razem z nowym identyfikatorem. Źródłem nie był żaden plik, tylko wyzwalacz w bazie, który zadziałał przy najbliższej operacji na komentarzach. Gdybyśmy poprzestali na plikach i nie sprawdzili wyzwalaczy ani sum kontrolnych, strona zostałaby „wyczyszczona” wyłącznie na oko. Wniosek narzucał się sam: warstwy trzeba zdejmować równolegle, w kolejności, która nie zostawia żadnej z nich czasu na reakcję, a całość zamykać testem sprawdzającym, czy cokolwiek się odradza.
Czyszczenie krok po kroku
Skoro warstwy odbudowują się nawzajem, remediacja musiała być zaplanowana jak jedna operacja, a nie seria niezależnych poprawek. Przebiegła w sześciu krokach.
| Krok | Na czym polegał |
|---|---|
| Izolowana reprodukcja | Pełną kopię witryny i bazy postawiliśmy lokalnie w Dockerze, w sieci odciętej od internetu. Backdoory nie mogły połączyć się z serwerem sterującym, więc analiza niczego nie zdradziła atakującemu i niczego nie ściągnęła z zewnątrz. |
| Weryfikacja sum kontrolnych | Rdzeń i wtyczki porównaliśmy z oficjalnymi wydaniami. To ta metoda, a nie przeszukiwanie po frazach, wskazała dwanaście podszytych plików rdzenia oraz wstrzyknięcia w skądinąd legalnych wtyczkach. |
| Skoordynowane czyszczenie | Pliki i bazę czyściliśmy razem, w ustalonej kolejności: najpierw loadery, potem trwałość w bazie, na końcu wyzwalacz i zadanie cykliczne. Chodziło o to, żeby żadna warstwa nie zdążyła odtworzyć poprzedniej. |
| Test reinfekcji | Po czyszczeniu wielokrotnie wywoływaliśmy stronę i sprawdzaliśmy, czy nie wracają konta, opcje ani pliki. Etap zamykało dopiero zero odrodzeń, a nie brak widocznych objawów. |
| Rotacja i utwardzenie | Przegenerowaliśmy klucze sesji, co unieważniło skradzione ciasteczka, zresetowaliśmy hasła, zaostrzyliśmy uprawnienia plików i usunęliśmy ryzykowne menedżery plików, które ułatwiały ponowne wejście. |
| Najpierw makieta, potem produkcja | Całą procedurę sprawdziliśmy na lokalnej kopii i dopiero sprawdzoną wersję powtórzyliśmy na żywej witrynie, z backupem trzymanym jako punkt przywrócenia. |
Właściwy scroll: fałszywy trop, od którego się zaczęło
Warto wrócić do zgłoszenia, od którego wszystko się zaczęło, bo zacinanie przewijania nie miało z infekcją nic wspólnego. Był to problem konfiguracji Elementora, potwierdzony w profilerze przeglądarki, a nie zgadnięty na oko. Na stronie działały jednocześnie dwa przyklejone elementy, nagłówek i dolny pasek z wezwaniem do działania, oba przeliczające swoją pozycję przy każdej klatce, a do tego efekt paralaksy reagujący naraz na przewijanie i na ruch myszy. Razem powodowały to, co w przeglądarce nazywa się thrashingiem układu: wymuszały ciągłe przeliczanie geometrii strony, przez co na wydajnym sprzęcie pojawiało się szarpanie, a na słabszym zawieszenie karty. Rozwiązanie było niewielkie. Ograniczyliśmy przyklejone elementy do widoku na komputerze i wyłączyliśmy paralaksę sterowaną myszą, a przewijanie wróciło do normy. O tym, ile powolna strona potrafi realnie kosztować w reklamie i w cierpliwości użytkowników, pisaliśmy osobno w tekście o tym, ile naprawdę kosztuje wolna strona.
Stan zastany i stan po naprawie
Zestawienie stanu zastanego z tym po naprawie pokazuje skalę różnicy lepiej niż opis.
| Stan zastany | Po remediacji |
|---|---|
| Ponad trzydzieści plików malware i dwanaście backdoorów w rdzeniu | Zero backdoorów, potwierdzone sumami kontrolnymi wobec oficjalnego wydania |
| Cztery fałszywe konta administratora z aktywnymi sesjami | Jedno prawdziwe konto administratora, wszystkie sesje unieważnione |
| Framework sterujący i samoodtwarzająca się trwałość w bazie | Wyzwalacz SQL, zadanie cykliczne i opcje z payloadem usunięte |
| Fałszywe okno „Cloudflare” podsuwane odwiedzającym | Rdzeń i wtyczki zweryfikowane oraz zaktualizowane |
| Zdalne wykonanie kodu dostępne przez jeden parametr w adresie | Uprawnienia zaostrzone, ryzykowne wtyczki usunięte |
Cała operacja odbyła się bez utraty treści. Strony, media i konfiguracja klienta zostały nietknięte, a witryna wróciła do sieci w pełni sprawna.
Co z tego wynika
Kilka rzeczy z tego przypadku warto zapamiętać niezależnie od tego, czy prowadzi się jedną stronę, czy kilkadziesiąt.
- Suma kontrolna widzi to, czego nie widzi skan po sygnaturach. Podszyte pliki rdzenia miały wiarygodne nazwy i przeszłyby każde przeszukiwanie po frazach. Porównanie z oficjalnym wydaniem to jedyny pewny sposób, żeby je wychwycić.
- Trwałość infekcji mieszka też w bazie. Wyzwalacze i autoładowane opcje odtwarzają dostęp niezależnie od plików. Wyczyszczenie samych plików daje spokój tylko pozorny.
- Warstwy zdejmuje się razem, nie po kolei. Kiedy elementy odbudowują się nawzajem, skutek przynosi wyłącznie skoordynowane usunięcie zamknięte testem reinfekcji.
- Przy backdoorze z prawami administratora zakłada się, że przejęte jest wszystko. Każde hasło i każdy klucz sesji traktuje się jako skompromitowany, a rotacja i przegenerowanie kluczy nie podlegają dyskusji.
- Objaw bywa fałszywym tropem. Zgłoszenie dotyczyło przewijania, a odsłoniło włamanie. Dlatego diagnostykę zaczynamy od zejścia z produkcji i spojrzenia na całość, a nie od naprawy pierwszego opisanego symptomu.
Masz problem z WordPressem?
Jeśli masz problem z WordPressem albo Twoja strona zachowuje się dziwnie, wolno się wczytuje, wyrzuca błędy, przekierowuje w nieznane miejsca albo pokazuje treści, których nikt tam nie wstawiał, zgłoś się do nas. Zaczniemy tak samo jak przy tej sprawie, od kopii i spokojnej diagnozy na swoim, zamiast grzebać w żywej witrynie. Pomożemy.



