Backdoor PHP: jak jeden zainfekowany plik zaraził 5 niezależnych stron na naszym serwerze
2026-11-07Podczas researchu do przebudowy jednego z naszych mniejszych projektów (przypomnienia o wywozie śmieci) znaleziono zaszyfrowany webshell — kod PHP, który deszyfruje zaszyty w sobie blob AES-256-CBC i wykonuje go przez eval() — podrzucony pod nazwami udającymi znane wtyczki WordPress na 5 niezależnych stronach w tym samym stacku Docker na naszym serwerze OMV. Wszystkie pliki skwarantowano i usunięto z produkcji. Poniżej pełna anatomia ataku: jak wyglądał kod, jak doszło do odkrycia i — co najciekawsze — dlaczego infekcja jednej strony rozlała się na cztery zupełnie niepowiązane projekty.
1. Jak do tego doszło
Przebudowywaliśmy stronę smieci.mgmt.eu.org — prosty serwis przypominający o wywozie śmieci. Podczas przeglądu kodu w katalogu projektu znalazł się plik forminator.php, który z nazwy udaje integrację z wtyczką WordPress Forminator. Jeden problem: ta strona nie jest nawet WordPressem. Plik nie pasował do niczego w projekcie.
To był sygnał do dalszego skanu. Przeszukaliśmy cały katalog ze stronami na serwerze (/home/user/appdata/web-serwer/html/) po charakterystycznych sygnaturach tego typu ataku: eval(, openssl_cipher_iv_length, identyczny rozmiar pliku. Wynik: 4 kolejne kopie tego samego ataku (identyczny kod) i jedna odmiana z inną obfuskacją.
2. Lista zainfekowanych plików
| # | Strona | Ścieżka | Nazwa maskująca | Rozmiar | Data mtime pliku |
|---|---|---|---|---|---|
| 1 | smieci.mgmt.eu.org | forminator.php |
wtyczka „Forminator” | 8326 B | 2026-04-20 |
| 2 | e-lapka.pl | wp-includes/Text/elementskit.php |
wtyczka „ElementsKit” | 8326 B | 2025-08-05 |
| 3 | gamingcenter.eu.org | banner/src/duplicator.php |
wtyczka „Duplicator” | 8326 B | 2021-04-07 |
| 4 | supla.eu.org | assets/speedycache.php |
wtyczka cache „SpeedyCache” | 8326 B | 2023-03-26 |
| 5 | klimastech.eu.org (tak, nasz blog) | wp-includes/sitemaps/class-wp-sitemaps-index-details.php |
plik rdzenia WP — nazwa prawie identyczna z prawdziwym class-wp-sitemaps-index.php, ale ten konkretny plik nigdy nie istniał w oficjalnym jądrze WordPress |
21181 B | 2020-02-06 |
Pliki #1–#4 mają identyczny base64-owy payload (ten sam ciąg na starcie, ten sam rozmiar) — to jeden atak, skopiowany pod różnymi nazwami na różne strony. Plik #5 to osobny, odrębny obfuskator, ale ten sam wzorzec działania.
Uwaga o datach: daty modyfikacji plików rozpięte są na lata 2020–2026 i prawie na pewno nie odpowiadają faktycznej dacie infekcji. Atakujący najpewniej ustawił stare
mtime(np.touch -r), żeby pliki nie rzucały się w oczy przy sortowaniu „ostatnio zmienione”. Rzeczywisty moment włamania mógł być jeden i ten sam dla wszystkich pięciu, mimo rozrzutu dat widocznych na dysku.
3. Jak działa backdoor — rozbiórka na części pierwsze
Kod jest obfuskowany losowymi nazwami zmiennych i funkcji, żeby każda kopia wyglądała inaczej na pierwszy rzut oka. Struktura i rozmiar (8326 B) są jednak identyczne we wszystkich czterech — typowy produkt automatycznego „generatora backdoorów” używanego do masowej infekcji shared-hostingu PHP. Poniżej ten sam kod, ze zdeobfuskowanymi nazwami, dla czytelności:

Mechanizm krok po kroku:
- W pliku zaszyty jest ~8 KB base64 — to zaszyfrowany blob (HMAC + salt + IV + ciphertext w jednym ciągu).
- Klucze AES i HMAC są derywowane z zaszytego w kodzie „hasła” (
PBKDF2, 120 000 iteracji SHA-256) połączonego z solą wyciągniętą z samego blobu. - Przed odszyfrowaniem kod weryfikuje integralność blobu przez
hash_equals()na HMAC-u — jeśli ktoś spróbuje podmienić payload bez znajomości klucza, backdoor po prostu nic nie zrobi (zero śladu w logach błędów). - Jeśli integralność się zgadza,
openssl_decrypt()z AES-256-CBC odszyfrowuje właściwy ładunek. - Odszyfrowany PHP trafia prosto do
eval().
Innymi słowy: zdalne wykonanie dowolnego kodu na żądanie. Kto wywoła ten plik przez HTTP (albo lokalnie), może uruchomić dowolny PHP — zakładając, że zna sekret, co sugeruje istnienie zdalnego panelu C2, który dekoduje się najpewniej do pełnego file managera / shella.
Wariant #5 (class-wp-sitemaps-index-details.php na klimastech.eu.org) ma inną obfuskację — pozornie losowy tekst przypominający komentarz, kończący się wywołaniem funkcji o wymownej nazwie recoverKeyMaterials(...), sugerującej kradzież kluczy lub danych uwierzytelniających.
4. Dlaczego jedna dziura zaraziła pięć niepowiązanych stron?
To było najciekawsze pytanie w całym śledztwie. Żadna z tych stron nie współdzieli ze sobą kodu — to niezależne projekty, część z nich nawet nie jest WordPressem. Odpowiedź leży w architekturze hostingu:

Jeden kontener web-php (PHP-FPM) obsługuje wszystkie strony na serwerze z jednego, współdzielonego konta systemowego www-data (zweryfikowane: docker exec web-php id → uid=82 gid=82 groups=82, identyczne dla każdego site’a). Efekt: zdalne wykonanie kodu (RCE) na JEDNEJ stronie = prawo zapisu do WSZYSTKICH pozostałych stron na tym samym serwerze.
Prawdopodobny punkt wejścia: wtyczka WordPress 3dprint-lite na klimastech.eu.org pozwalała przesyłać pliki (do 100 MB) bez logowania — podatność znaleziona już wcześniej w naszym własnym pentescie infrastruktury i wtedy naprawiona tylko częściowo (rate-limit w nginx, bez wymuszenia logowania w samej wtyczce). To realistyczny wektor dla pierwszego backdoora, który stamtąd mógł się rozlać na pozostałe cztery strony właśnie dzięki współdzielonemu www-data.
5. Co pokazały logi
Tu niestety trafiliśmy na ścianę. Dostępna historia logów nginx sięga tylko do 1 czerwca 2026, a jeden z zainfekowanych plików (e-lapka.pl) ma mtime z sierpnia 2025 — daleko poza oknem logów. W dostępnym oknie (czerwiec–listopad 2026) nie ma ani jednego żądania HTTP do nazw tych pięciu plików. Oznacza to jedno z dwojga: albo backdoor nie był używany w ostatnich miesiącach, albo trigger szedł inną drogą (bezpośrednio z panelu C2, nielogowaną standardowo, albo lokalnie z poziomu samego hosta).
6. Co zrobiliśmy
- Skopiowaliśmy wszystkie 5 plików do kwarantanny poza webrootem (nic z nich nie zostało uruchomione w trakcie analizy).
- Usunęliśmy je ze wszystkich pięciu stron produkcyjnych.
- Zweryfikowaliśmy przed usunięciem, że plik #5 nigdy nie był częścią prawdziwego jądra WordPress (porównanie z czystym, świeżo pobranym WP 7.1.4) i że żaden kod na klimastech.eu.org go nie
require/includeuje — bezpieczne do usunięcia. - Sprawdziliśmy wszystkie pięć stron po usunięciu — działają normalnie, bez przerwy w dostępności.
7. Co jeszcze przed nami
Znalezienie i usunięcie plików to dopiero połowa roboty. Lista otwartych zadań:
- Rotacja haseł i kluczy używanych przez wszystkie pięć stron (webhooki, bazy danych, SMTP) — backdoor miał pełny
eval(), więc mógł je odczytać w dowolnym momencie. - Rozdzielenie PHP-FPM per-site (osobny pool / UID na każdą stronę) — to jedyny sposób, żeby kompromitacja jednej strony przestała automatycznie oznaczać kompromitację wszystkich pozostałych. To naprawa architektoniczna, nie punktowa, i właśnie nad nią teraz pracujemy.
- Pełna naprawa
3dprint-lite— wymuszenie logowania bezpośrednio w kodzie wtyczki, nie tylko rate-limit na poziomie nginx. - Pełny skan malware całego katalogu stron (np. Wordfence / ClamAV) — nasz skan szukał wyłącznie sygnatur tego konkretnego ataku, nie wszystkich możliwych backdoorów.
Wnioski dla każdego, kto trzyma kilka stron na jednym hoście
Trzy rzeczy, które zabieramy z tego incydentu:
- Współdzielony
www-datato jedna duża powierzchnia ataku, nie pięć małych. Jeśli Twój hosting (własny albo shared) uruchamia PHP wielu niepowiązanych stron pod jednym użytkownikiem systemowym, włamanie na najsłabszą z nich jest włamaniem na wszystkie. - Daty modyfikacji plików to nie dowód. Atakujący potrafią manipulować
mtime, żeby backdoor wyglądał jak stary, nieszkodliwy plik. Skanuj po sygnaturach i rozmiarze, nie po „co ostatnio się zmieniło”. - Nieautoryzowany upload w jednej wtyczce = problem całej infrastruktury. Rate-limit w nginx to plaster, nie leczenie — jeśli punkt wejścia pozostaje otwarty, prędzej czy później ktoś przez niego wejdzie ponownie.