Jeśli po włamaniu WordPress zwraca 403 na wp-login, a podstrony 404, to prawie na pewno atakujący nadpisał główny plik .htaccess. Rozwiązanie: odtworzyć .htaccess z regułami WordPressa, usunąć webshelle (także te ukryte w katalogach rdzenia) i zablokować wykonywanie PHP tam, gdzie nie jest potrzebne. Poniżej opisujemy taki przypadek z hostingu cPanel/LiteSpeed, na którym mieliśmy tylko FTP, bez SSH.
Jakie objawy daje podmieniony .htaccess?
- 403 na stronie logowania. Właściciel nie może wejść do panelu, mimo że zna hasło.
- 404 na wszystkich podstronach. Działa tylko strona główna.
- Nowe pliki index.php w dziwnych miejscach. Każdy z własnym .htaccess obok.
Te dwa błędy mają jedną przyczynę. Atakujący zastąpił główny .htaccess własnym: zablokował wykonanie każdego pliku *.php poza index.php i przy okazji usunął reguły rewrite WordPressa. Bez tych reguł serwer nie wie, że adres w rodzaju /kontakt/ ma obsłużyć index.php, więc szuka katalogu, którego nie ma, i zwraca 404. Plik wp-login.php wpadł pod blokadę, stąd 403. Właściciel, który w takiej sytuacji resetuje hasło albo wyłącza wtyczki przez FTP, traci czas, bo problem siedzi w jednym pliku konfiguracyjnym serwera.
Jak znaleźć webshell w WordPressie bez SSH?
Webshelle z funkcją zdalnego wykonywania poleceń i wgrywania plików leżały w katalogu głównym. To było łatwe. Trudniejsze były kopie tego samego shella zapisane jako index.php z plikiem .htaccess zawierającym „Allow from all” w katalogach, do których nikt nie zagląda:
wp-includes/blocks/...wp-admin/css/colors/...wp-content/themes/twentytwentyfour/patterns/
Nazwa nie jest przypadkowa. Reguła atakującego przepuszczała tylko pliki index.php, więc jego kopie działały dalej, a lokalny .htaccess z „Allow from all” znosił blokady z katalogów wyżej.
Najpewniejsza metoda wykrywania to porównanie listy plików z czystym rdzeniem tej samej wersji. Przy innym włamaniu znaleźliśmy w ten sposób 27 webshelli z jednego dnia, w tym menedżer plików, którego hasło było sumą MD5 pustego ciągu (czyli wchodziło się bez hasła), i fałszywe obrazki: nagłówek JPEG lub GIF, a dalej kod PHP. Bez SSH pobierasz pliki przez FTP i porównujesz lokalnie:
cd czysty-wordpress && find . -type f | sort > ../czysty.txt
cd ../pobrana-strona && find . -type f | sort > ../strona.txt
comm -23 ../strona.txt ../czysty.txt # pliki, których nie ma w oryginaleWynik obejmie też wtyczki, motywy i uploads, ale w wp-admin i wp-includes każdy dodatkowy plik jest podejrzany. Samo wp core verify-checksums nie wystarczy: sprawdza zmienione pliki rdzenia, a nowych plików w podkatalogach nie wykrywa.
Którędy atakujący dostał się do strony?
Definitywnie tego nie ustaliliśmy. Access logi na tym hostingu były symlinkiem do katalogu domlogs i nie dało się ich pobrać przez FTP. Zostały statystyki awstats z panelu:
- 38 tys. żądań do xmlrpc.php i 8 tys. do wp-login.php w ciągu miesiąca, czyli zgadywanie haseł,
- 15 wywołań plugin-editor.php miesiąc wcześniej.
Najbardziej prawdopodobny scenariusz to złamane hasło administratora, a potem wbudowany edytor plików w panelu, którym da się zapisać dowolny kod PHP w pliku wtyczki lub motywu bez żadnego dodatkowego narzędzia. Bez pełnych logów to jednak hipoteza, dlatego zablokowaliśmy zarówno xmlrpc, jak i edytor plików.
Masz ten sam problem? Usuwamy webshelle także wtedy, gdy hosting daje tylko FTP i panel. Opisz sytuację →
Jak naprawić .htaccess po włamaniu do WordPressa?
Zaczęliśmy od nowego .htaccess w katalogu głównym: standardowe reguły rewrite WordPressa plus linia z handlerem PHP, którą hosting wstawia dla wybranej wersji PHP (najlepiej skopiować ją z panelu albo z kopii sprzed włamania). Do tego blokady:
| Co blokujemy | Po co |
|---|---|
| xmlrpc.php | główny kanał zgadywania haseł w tym przypadku |
| pliki wrażliwe i ukryte (z kropką na początku nazwy) | wyciek haseł do bazy |
| PHP w wp-includes | tam nie ma czego wywoływać bezpośrednio |
| PHP w uploads (osobny .htaccess) | uploads to ulubione miejsce na shelle |
| zapytania ?author= | enumeracja loginów użytkowników |
| Options -Indexes | listing katalogów |
Doszły nagłówki HSTS, X-Frame-Options i X-Content-Type-Options: nosniff. Blokada PHP w uploads wygląda tak (Apache 2.4 i LiteSpeed):
# wp-content/uploads/.htaccess
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>W samym WordPressie: define('DISALLOW_FILE_EDIT', true); w wp-config.php (wyłącza edytor, którym prawdopodobnie wszedł atakujący), nowe klucze salts, które wylogowują wszystkie sesje, i usunięcie wtyczki wp-file-manager. Menedżer plików w panelu WordPressa to gotowe narzędzie dla każdego, kto przejmie konto admina.
Co jeszcze znaleźliśmy po sprzątaniu?
Publiczne kopie zapasowe. Archiwum Duplicatora (2,6 GB), plik .wpress i wp-config.php.bk leżały w katalogu strony i zwracały HTTP 200, czyli każdy mógł je pobrać razem z hasłem do bazy. Przenieśliśmy je poza katalog publiczny.
Na koniec uruchomiliśmy monitoring integralności plików (FIM) z crona w cPanelu co 30 minut, bo brak SSH nie oznacza braku crona. Bazą odniesienia jest 42 814 plików. Każdy nowy lub zmieniony plik wychodzi w raporcie, więc kolejny webshell nie przeleży na serwerze miesięcy, zanim ktoś go zauważy.
Kilka stron WordPress na jednym hostingu: czy infekcja się przenosi?
Tak. Na jednym koncie hostingowym wszystkie strony działają jako ten sam użytkownik systemowy, więc webshell wgrany do jednej instalacji może czytać i zapisywać pliki wszystkich pozostałych, łącznie z ich wp-config.php i hasłami do baz.
W innym przypadku na jednym koncie było ok. 41 stron WordPress. Ukryte konto administratora o loginie wordpress było aktywne na 11 z nich, webshell siedział w uploads/2023/index.php, a atakujący na bieżąco wykorzystywał lukę w dodatku do motywu, która pozwalała wgrywać pliki bez logowania. Efekt: pliki .yo-<hex>.php w uploads. Zrobiliśmy wtedy:
- mu-plugin, który odrzuca anonimowe żądania do admin-ajax zawierające kod PHP lub plik .php (403 i wpis w logu),
- zakaz wykonywania PHP w uploads na wszystkich stronach,
- usunięcie ukrytego konta na 11 stronach,
- monitoring co godzinę: lista administratorów według uprawnień (capabilities, nie samej roli), pliki PHP w uploads i sumy kontrolne rdzenia.
Uwaga: czyszczenie jednej strony na współdzielonym koncie nic nie daje. Atakujący wraca z sąsiedniej instalacji.
Kiedy zlecić usunięcie wirusa ze strony?
Gdy nie masz dostępu do panelu, gdy hosting nie daje SSH albo gdy na koncie stoi kilka stron. Wtedy ręczne kasowanie widocznych plików zwykle kończy się powrotem infekcji po kilku dniach. Ogólny przebieg sprzątania opisaliśmy w artykule wirus na stronie WordPress, a tu jest nasza usługa usuwania malware z WordPressa. ASAPDevs zaczyna od diagnozy i zabezpieczenia śladów, a dopiero potem kasuje.
