Okładka: backdoor PHP rozprzestrzeniony na 5 stron

Backdoor PHP: jak jeden zainfekowany plik zaraził 5 niezależnych stron na naszym serwerze

2026-11-07 0 Przez klimas

Podczas 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:

Zdeobfuskowany schemat działania backdoora PHP

Mechanizm krok po kroku:

  1. W pliku zaszyty jest ~8 KB base64 — to zaszyfrowany blob (HMAC + salt + IV + ciphertext w jednym ciągu).
  2. 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.
  3. 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).
  4. Jeśli integralność się zgadza, openssl_decrypt() z AES-256-CBC odszyfrowuje właściwy ładunek.
  5. 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:

Diagram architektury: jeden PHP-FPM obsługuje 5 stron jako www-data

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:

  1. Współdzielony www-data to 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.
  2. 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”.
  3. 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.