Aktualizacja — maj 2026. Krótko po pierwszym opisaniu sprawy ten sam sklep został trafiony ponownie. Reinfection nie przyszła przez backdoor, który przeoczyliśmy — przyszła przez front door: niezałataną lukę na poziomie aplikacji (SQL injection w module Wishlist blockwishlist połączone ze Smarty MySQL-cache RCE, CVE-2022-36408), którą nasze czyszczenie na miejscu ograniczyło, ale nigdy nie zamknęło. Usunięcie malware to nie to samo co zamknięcie wektora wejścia. Głębsza remediation na poziomie aplikacji — virtual-patching konkretnych podatności i zamknięcie origin za Cloudflare — to osobny temat opisany w Advanced PrestaShop Hardening for Stores You Can't Upgrade Yet. Wniosek pozostaje bez zmian: containment kupuje czas; migracja z 1.7.x jest leczeniem.

To prawdziwy incydent ze sklepu PrestaShop 1.7.x, który odzyskiwaliśmy. Identyfikatory są zanonimizowane, ale flow kontroli, lokalizacje plików, struktura payloadu i logika detekcji są zachowane dokładnie. Celem opisu nie jest straszenie — chodzi o konkretny punkt odniesienia dla innych właścicieli sklepów i developerów, jak nowoczesny atak w stylu Magecart na PrestaShop wygląda od środka, żeby po zobaczeniu tych samych kształtów we własnych plikach rozpoznać je w minuty, nie w dni.

Właściciel skontaktował się z nami, bo strona checkout pokazywała formularz płatności, którego nie rozpoznawał. Wyglądał jak formularz kart Stripe Elements. Sklep faktycznie używa Stripe, więc podobieństwo wizualne było wystarczająco bliskie, że przez prawie dwa dni nikt go nie zakwestionował. Gdy weszliśmy, skimmer po cichu kopiował dane kart od każdego klienta, który dotarł do checkout. Ten artykuł przechodzi przez dochodzenie w kolejności, w jakiej naprawdę się rozwijało — pierwsza wskazówka, payload serwerowy, trik eksfiltracji, JavaScript, root compromise i w końcu punkt wejścia — bo właśnie kolejność jest lekcją.

Pierwsza wskazówka: formularz płatności, którego nie powinno być

Stripe Elements renderuje pola karty w cross-origin iframes. Strona rodzica nie może odczytać tego, co klient do nich wpisuje — ta izolacja jest całym powodem istnienia Elements i tym, co chroni poprawnie zintegrowany formularz Stripe przed skimmingiem ze skompromitowanego storefrontu. Dlatego pierwsze pytanie przy każdym podejrzeniu skimmera jest proste: czy pola karty są w iframe, czy są surowymi inputami na stronie?

Formularz, który oglądaliśmy, używał surowych, first-party inputów HTML:

Wstrzyknięte poleCo Ci mówi
<input id="cardNumber" onkeyup="smenu(this)">Brak iframe — strona rodzica może czytać każdy klawisz
<input id="cc_cid" onkeyup="smenu(this)">Handler onkeyup na każdym polu, wywołujący niestandardową funkcję smenu()
<button onclick="processF()">PLACE ORDER</button>Niestandardowy handler submit, który omija prawdziwy moduł płatności

Trzy natychmiastowe sygnały: brak iframe, handler keystroke na każdym inputcie i subtelna niezgodność języka między wstrzykniętym formularzem a resztą sklepu. Legalny moduł Stripe nadal był zainstalowany i skonfigurowany — fałszywy formularz został po prostu namalowany na nim. Korzyść z poznania tej jednej zasady (prawdziwe bramki używają iframes; surowe inputy kart na checkout PrestaShop są prawie zawsze złośliwe) jest taka, że każdy merchant może sprawdzić to sam, dziś, przez inspector przeglądarki i bez specjalnych narzędzi — patrz lista wskaźników na końcu wpisu.

Odnalezienie payloadu po stronie serwera

Keylogger w przeglądarce ma sens tylko wtedy, gdy może wysłać to, co przechwyci, gdzieś dalej. Przeszukaliśmy cały katalog config/ pod kątem podejrzanych wzorców i znaleźliśmy ten sam blok dopisany na początku każdego pliku PHP w tym katalogu: snippet obserwujący pole $_POST['order_llx'], a gdy je widział, przekazujący zawartość przez curl do dwóch zakodowanych base64 URL-i z wyłączonym CURLOPT_SSL_VERIFYPEER, po czym usuwający pole, żeby nic niżej tego nie zauważyło.

Trzynaście plików w config/ zawierało ten blok i każdy miał ten sam czas zmiany inode — co do tej samej sekundy. Atakujący wdrożył to skryptem. Dwa stringi base64 dekodowały się do adresów IP dwóch serwerów u dużego azjatyckiego dostawcy chmury: endpointu eksfiltracji. Jeden plik, settings.inc.php, został podmieniony w całości — został tylko złośliwy kod — a sklep nadal działał, bo w PrestaShop 1.7 prawdziwa konfiguracja bazy danych mieszka w app/config/parameters.php, nie w przestarzałym settings.inc.php. Atakujący albo to wiedział, albo miał szczęście.

Dwustopniowy trik eksfiltracji

Naprawdę sprytna część tego ataku to sposób, w jaki dane wychodzą. Większość Content Security Policies, ad blockerów i rozszerzeń anti-skimmer jest zbudowana tak, żeby łapać przeglądarkę rozmawiającą bezpośrednio z serwerem atakującego. Ten skimmer nigdy tego nie robi. Skompromitowany sklep działa jako relay:

  • Krok 1 (browser → store). Handler processF() fałszywego formularza wysyła przechwycone dane karty przez POST do / na własnej domenie klienta, w polu o nazwie order_llx. Z perspektywy przeglądarki to zwykły ruch first-party — ten sam origin, brak CORS, nic, co CSP nastawiona tylko na third-party miałaby oznaczyć.
  • Krok 2 (store → C2). Wstrzyknięty PHP w config/ łapie to pole po stronie serwera i przesyła je przez curl do IP atakującego. To request server-to-server, którego przeglądarka nigdy nie widzi: nie pojawia się w DevTools, w zakładce network ani dla admina sieci obserwującego połączenie klienta.

To całe obejście. Admin sieci widzi połączenie z legalnym sklepem i przestaje drążyć; CSP blokująca tylko third-party scripts nie łapie nic. Ten same-origin relay jest jednym z trudniejszych kształtów Magecart do wykrycia po stronie klienta. Warto uczciwie powiedzieć, że nie jest jedynym kształtem — Sansec i inni trackerzy nadal widzą eksfiltrację bezpośrednio z przeglądarki, ukryte magazynowanie po stronie serwera i warianty delayed-pull działające równolegle, bez jednego dominującego wzorca — ale relay jest tym, który pokonuje większość gotowych zabezpieczeń client-side.

JavaScript skimmer i różnica rozmiaru, która go zdradziła

Sam fałszywy formularz mieszkał w theme. Przeszukaliśmy unikalne tokeny, które już widzieliśmy — cc_owner, processF, smenu — i znaleźliśmy injection dopisany do ps_shoppingcart.js w theme. Sama różnica rozmiaru pliku była smoking gun: czysty ps_shoppingcart.js ma około 2,7 KB, a zainfekowana kopia około 340 KB. Jeden blob 337 KB został doklejony na końcu 74 różnych plików JavaScript w parent i child theme — każdego theme.js, każdego custom.js, każdego front-end script modułu w vendored folderach modułów w theme.

Payload był zaciemniony, ale niezbyt sprytnie: hex-encoded string array (nazwany _0x5aa5, 225 pozycji) referencjonowany po indeksie w całym kodzie, więc document[_0x5aa5[39]](_0x5aa5[158]) po prostu rozwiązywało się do document.getElementById("cardNumber"). Zdekodowanie tablicy oddaje cały zamiar — wpisy dla cardNumber, exp-date, cc_owner, cc_cid, pól billingowych, nazwy pola eksfiltracji order_llx i taga kampanii Zhang. Skimmer składa pipe-delimited string ze wszystkich przechwyconych pól, hex-encoduje go, owija base64 i wysyła jako FormData do /. Kilka pól-przynęt (products_hash, amount_hash, billing_hash) jedzie razem jako losowy szum, dzięki czemu POST wygląda w logach jak zwykły beacon analityczny.

Pułapka skompilowanych assetów (specyficzna dla PrestaShop)

Podmieniliśmy zainfekowany ps_shoppingcart.js na czystą kopię. Fałszywy formularz nadal się pojawiał. To pułapka, która łapie ludzi czyszczących konkretnie sklep PrestaShop, więc warto rozumieć ją precyzyjnie.

Pipeline CCC PrestaShop (Combine, Compress, Cache) bundluje front-office scripts zarejestrowane dla bieżącej strony — wszystko zadeklarowane przez registerJavascript bez async/defer — w połączone pliki typu bottom-<hash>.js pod themes/<theme>/assets/cache/. Zainfekowane skrypty były zarejestrowane dokładnie w ten sposób, więc złośliwy kod został wypieczony w skompilowanych bundle'ach, gdy źródło nadal było zainfekowane. Dopóki nie wyczyściliśmy tego katalogu cache, przeglądarka dostawała skimmer ze skompilowanego bundle'a niezależnie od tego, co robiliśmy z plikiem źródłowym. Naprawą była podmiana całego drzewa themes/ ze znanego czystego źródła i wyczyszczenie każdego cache assetów — themes/<theme>/assets/cache/, var/cache/ i var/compile/ — plus dowolny object cache Redis albo Memcached. Cokolwiek mniej zostawia bundle regenerujące się z zainfekowanego źródła.

Większy problem: root compromise

Podczas polowania na kolejne payloady skimmera uruchomiliśmy find / -perm -4000 -type f i dostaliśmy nieprzyjemną niespodziankę: dwa binaria ELF leżące w webroot, zamaskowane rozszerzeniami .js. Bit SUID ustawiony, właściciel root, po 1,7 MB, identyczny SHA256 — i wcale nie JavaScript, tylko wykonywalne pliki x86_64 ELF (file raportował setuid ELF 64-bit LSB executable, statically linked, stripped). Cztery kolejne SUID-root binaria były w /usr/bin z losowymi nazwami, plus jedno nazwane pkexem — literówka o jedną literę względem legalnego pkexec, zaprojektowana tak, żeby prześlizgnąć się przy pobieżnym ls.

To był moment, w którym incydent zmienił kategorię. Utworzenie binarki SUID root wymaga roota. PHP działające jako użytkownik strony nie może tego zrobić, niezależnie od tego, jak puste jest disable_functions albo jak luźne jest open_basedir. Samo istnienie tych plików dowodziło, że skompromitowany był root, nie tylko użytkownik web — a to oznacza, że czyszczenie webrootu nigdy nie może być końcem pracy, bo persistence może ukrywać się gdziekolwiek na hoście. Dla właściciela sklepu praktyczne tłumaczenie jest brutalne: w tym punkcie jedynym w pełni defensywnym stanem końcowym jest reprovision z czystego obrazu OS, migracja zweryfikowanie czystego kodu i danych, rotacja każdego sekretu oraz traktowanie oryginalnego hosta jako tymczasowej powierzchni containment utrzymywanej przy życiu tylko tak długo, jak trzeba obsłużyć klientów.

Rzeczywisty punkt wejścia: web shell w index.php

Wyczyściliśmy skimmer i wyciągnęliśmy binarki backdoor, ale nadal nie wiedzieliśmy, jak atakujący dostał się na początku. Access logi pokazywały jedno IP wykonujące requesty POST / przez kilka tygodni, a rozmiary odpowiedzi były odciskiem palca:

Rozmiar odpowiedziCo robił atakujący
~46 KBPełna strona główna — probing exploita ze złymi parametrami
1–2 KBOutput komend (ls, cat, whoami) — reconnaissance
~695 BPotwierdzenia zapisu pliku — plantowanie payloadu
~36 KBJedna większa odpowiedź, nadal dużo poniżej dumpa bazy

Ten rozkład powiedział nam dwie rzeczy. Po pierwsze, POST / sam był endpointem wykonania kodu — zwykłe requesty strony głównej nie produkują stałego strumienia 695-bajtowych potwierdzeń zapisu pliku. Po drugie, pełny eksport tabeli klientów miałby co najmniej setki kilobajtów, a nic w logach nawet się do tego nie zbliżało; atakujący używał shella do zapisu plików, nie do masowego wyciągania danych. Ta rozbieżność jest realnie uspokajającym dowodem, który można pokazać zaniepokojonemu właścicielowi, i właśnie takie ustalenie access logi dają za darmo, jeśli czytasz rozmiar odpowiedzi, a nie tylko status code.

Endpoint doprowadził nas do index.php. Kopia na live site była w pewnym momencie wyczyszczona (prawdopodobnie przez wcześniejszą próbę odzyskiwania), ale dev site w sąsiednim katalogu współdzielącym ten sam docroot nadal miał oryginał. Zaczynał się od zaciemnionego loadera: jeden string literal działający jako alfabet, z nazwami niebezpiecznych funkcji (create_function, assert, base64_decode) składanymi znak po znaku przez indeksowanie do niego — omijając naiwny grep. Loader dekodował mały payload base64, budował wykonywalną closure PHP w runtime i wywoływał ją. Zdekodowany payload był samym web shellem: każdy request, którego parametr product_id pasował do hardcoded MD5 hash, dostawał eval() dowolnego PHP przekazanego w parametrze image_id. Nazwy parametrów są celowo dobrane tak, żeby wyglądały w logach jak zwykły ruch ecommerce.

Jeden szczegół zasługuje na osobne zdanie, bo jest przypadkowym prezentem aktualizacji: loader zależy od create_function i ewaluacji stringowego assert(), które PHP usunęło w 8.0. Na nowoczesnym hoście PHP 8 ten backdoor sam z siebie wywaliłby fatal error. Przeniesienie tego codebase na PHP 8 niechcący złamałoby shella — kolejny powód, dla którego prawdziwa naprawa jest do przodu, nie w bok.

Timeline i co z niego wynika

Nie mogliśmy datować pierwotnego plantowania web shell — wyprzedzało retencję dostępnych logów — ale resztę dało się czysto odtworzyć z timestampów plików i access logów:

Dzień względem wykryciaZdarzenieDowód
−60 lub wcześniejWeb shell posadzony w index.phpWyprzedza retencję logów
−25SUID-root binaria wrzucone do webrootTimestampy utworzenia plików
−22SUID-root binaria wrzucone do /usr/binTimestampy utworzenia plików
−10 do −5Reconnaissance przez web shellOdpowiedzi POST /, 695–1844 B
−2Skimmer JS wdrożony do 74 plików themeTimestampy zmian inode
−2 (≈30 min później)Skimmer PHP wdrożony do 13 plików configTimestampy zmian inode
0Właściciel zgłasza podejrzany formularzInspekcja wizualna

25-dniowa luka między root compromise a wdrożeniem skimmera jest najbardziej mówiącą linią w tej tabeli. Pasuje do sytuacji, w której initial access i monetyzacja są osobnymi etapami — dostęp pojawił się pierwszy, payload do skimmingu kart przyszedł tygodnie później. Nie możemy udowodnić wyłącznie na podstawie tych danych, czy dostęp został sprzedany drugiej grupie, czy po prostu kolejkowany przez tego samego operatora pracującego przez listę celów, ale lekcja dla obrońcy jest ta sama: ciche tygodnie nie są bezpieczeństwem, są odliczaniem. Sklep, który złapie intrusion w tych tygodniach, nie traci nic; sklep, który czeka na objaw, traci każdą kartę przetworzoną po drodze.

Remediation, w kolejności, która ma znaczenie

Dwa ograniczenia ukształtowały to odzyskiwanie i są to ograniczenia większości prawdziwych sklepów. Site był live i obsługiwał zamówienia, więc nie mógł zgasnąć na godziny. A upgrade 1.7.x → 8.x/9.x to projekt wielotygodniowy — kompatybilność modułów, refaktoring theme, przepięcie płatności — nie robota na jeden dzień. Planem było więc containment i hardening teraz, z czystym rebuildem przygotowywanym równolegle. Pełny runbook krok po kroku (zabezpieczenie dowodów, rotacja credentiali, polowanie na backdoory, firewalling IP C2, usunięcie wystawionych narzędzi adminowych, strojenie WAF) jest osobnym dokumentem — trzymamy go jako PrestaShop Security Hardening Checklist, żeby pozostawał aktualny. To, co jest specyficzne dla tego incydentu i warte zapamiętania, to kolejność i kilka ruchów świadomych PrestaShop:

  • Zabezpiecz dowody, zanim czegokolwiek dotkniesz. Zrób snapshot dysku (albo minimum tar webroot, /etc i /var/log), zarchiwizuj access i error logi oraz policz sha256sum każdego podejrzanego pliku. Będziesz ich potrzebować do post-mortem, do ewentualnego zapytania payment processora i do nieuniknionego drugiego przejścia, gdy znajdziesz przeoczony backdoor.
  • Najpierw zatrzymaj krwawienie widoczne dla klienta. Usuń PHP injection z 13 plików config, przywróć config/settings.inc.php z czystej kopii dokładnie tego samego wydania PrestaShop (i trzymaj rotację DB/cookie skupioną na app/config/parameters.php, gdzie PS 1.7+ faktycznie trzyma tę konfigurację), podmień całe drzewo themes/ z czystego źródła i wyczyść każdy katalog cache oraz OPcache — inaczej skompilowane bundle nadal serwują skimmer, jak wyżej.
  • Potem poluj na persistence na całym hoście, nie tylko w webroot. Ponieważ root był skompromitowany, niebezpieczne artefakty mogą żyć w /etc/cron*, systemd timers i units, ~/.ssh/authorized_keys każdego użytkownika, /etc/sudoers.d/, profilach powłoki, plikach vhost i tabeli MySQL mysql.user plus nowych eventach lub triggerach. Wszystko zmodyfikowane w oknie kompromitacji jest podejrzane.
  • Uczyń pliki punktów wejścia immutable. chattr +i na index.php i wszystkich config/*.php. Przyszły web shell nie może przepisać pliku immutable bez wcześniejszego chattr -i, a to wymaga roota. (Zastrzeżenie: działa tylko na filesystemach obsługujących extended attributes — ext4/xfs/btrfs są w porządku; niektóre container overlay i konfiguracje ZFS nie.)

Gdy root został dotknięty, in-place cleaning jest containment, nie leczeniem — a jeśli wektor reinfection jest niezałataną podatnością aplikacji, a nie przeoczonym plikiem, powyższy runbook Cię nie uratuje. Ten problem na warstwie aplikacji (virtual-patching konkretnych CVE, zamknięcie origin za Cloudflare, poprawne ograniczenie runtime PHP) jest dokładnie tym, co opisuje Advanced PrestaShop Hardening for Stores You Can't Upgrade Yet. A jeśli dane kart dotarły do C2 — zakładaj, że tak było, dopóki nie udowodnisz inaczej — obowiązki raportowe wobec payment processora zaczynają się natychmiast; pełna sekwencja jest w naszym Data Breach Response guide.

Indicators of compromise — czego szukać grepem

Uruchom wyszukiwanie z root sklepu i traktuj każde trafienie jako dowód do sprawdzenia, nie jako automatyczny wyrok samo w sobie:

rg -n "order_llx|processF|smenu|cc_owner|cc_cid|CURLOPT_SSL_VERIFYPEER|base64_decode" \
  config themes modules

find config themes modules -type f \( -name '*.php' -o -name '*.js' \) -mtime -14 -ls

Jeśli prowadzisz sklep PrestaShop i chcesz sprawdzić tę konkretną rodzinę, to jest zestaw high-signal. Duże recursive greps uruchamiaj na snapshotcie albo w trybie read-only, jeśli możesz — ciężki scan na zajętym webroot może podbić I/O i uruchomić własne alerty.

Gdzie patrzećWskaźnik
Źródła PHP / JSrg -n "order_llx|_0x5aa5|processF|smenu|__Pres_[il]dk" /var/www
Injection w plikach configDosłowne @deprecated 1.7$ar=[ dopisane na początku plików w config/
Zaciemnione loaderyŁańcuchy eval(base64_decode(…)), assert(base64_decode(…)) albo create_function(…base64…)
SUID backdoorsfind / -perm -4000 -type f 2>/dev/null (uruchom jako root)
ELF udający JSfind /var/www -name '*.js' -exec file {} + | grep ELF
Prepend persistenceauto_prepend_file / auto_append_file w .htaccess albo .user.ini

A w przeglądarce, na wyrenderowanym checkout: surowe pola <input> z ID cc_owner, cardNumber, cardExpiry albo cc_cid (prawdziwe formularze Stripe/Adyen/Braintree używają iframes — surowe inputy kart to red flag), ustawiane cookie o nazwach __Pres_idk albo __Pres_ldk oraz index.php, który zaczyna się od zaciemnionej zmiennej w stylu <?php $e4d6= z jedną literą plus hex zaraz po tagu otwierającym. Tag kampanii Zhang (base64 Wmhhbmc=) pojawia się w payloadzie eksfiltracji, ale jest zbyt generyczny, żeby ufać mu samemu — nadaj mu wagę tylko wtedy, gdy współwystępuje z innym wskaźnikiem powyżej. Do głębszych sweepów skieruj php-malware-finder, publiczne reguły YARA Sucuri/ESET albo file-integrity monitor (AIDE, Wazuh, OSSEC) na webroot — ale pomiń skanery specyficzne dla Magento; ich sygnatury nie dopasują ataku o kształcie PrestaShop.

O co naprawdę chodzi w tym incydencie

Web shell był raną. Skimmer był widocznym objawem. Wyczyszczenie skimmera bez znalezienia shella zostawiłoby drzwi szeroko otwarte do ponownego wdrożenia tego samego payloadu — albo sprytniejszego — w kilka dni, i dokładnie tak wydarzyła się reinfection z początku tego wpisu. Najważniejszy nawyk, którego uczy ten incydent, to ciągnąć wątek dalej: fałszywy formularz karty prowadzi do wstrzykniętych plików config, one prowadzą do pułapki compiled-bundle, ta prowadzi do SUID-root binaries, a te prowadzą do web shell w index.php. Zatrzymaj się na dowolnej warstwie powyżej ostatniej, a wyczyściłeś objaw, nie zamknąłeś ataku.

A jeśli dziś używasz PrestaShop 1.7.x, uczciwe ujęcie jest takie: jesteś celem, a exploity używane do posadzenia pierwotnego shella są wieloletnie, dobrze znane i spakowane w automatyczne skanery. Migracja do wspieranej, w pełni załatanej gałęzi PrestaShop — z aktualnymi modułami i theme oraz przebudowanym i utwardzonym hostem — jest trwałą ścieżką. W międzyczasie — realistycznie tygodnie albo miesiące dla nietrywialnego sklepu — hardening z hardening checklist i środki warstwy aplikacji z advanced hardening for stores you can't upgrade yet nie czynią Cię niezniszczalnym, ale razem podnoszą koszt ataku i — równie ważne — robią intrusion na tyle głośne, że masz realną szansę złapać je w cichych tygodniach, zamiast po tym, jak karty znikną.

Oficjalne referencje PrestaShop

Oficjalne wskazówki znajdziesz w alercie bezpieczeństwa PrestaShop o digital skimmers, poście projektu o poważnej podatności bezpieczeństwa PrestaShop, nocie z 2025 roku o atakach SQL injection i wstrzykniętym JavaScript oraz najlepszych praktykach zabezpieczania sklepu PrestaShop. Żadne z nich nie raportuje tego konkretnego incydentu, ale są użytecznym oficjalnym kontekstem dla kontroli skimmerów, rotacji credentiali i obowiązków eskalacyjnych.

Udostępnij ten wpis:
David Miller

David Miller

Ponad dekada praktycznego doświadczenia z PrestaShop. David tworzy wydajne moduły e-commerce skupione na SEO, optymalizacji zamówień i zarządzaniu sklepem. Pasjonat czystego kodu i mierzalnych rezultatów.

Spodobał Ci się ten artykuł?

Otrzymuj nasze najnowsze porady, przewodniki i aktualizacje modułów prosto na swoją skrzynkę.

Komentarze

Brak komentarzy. Bądź pierwszy!

Bądź pierwszy: zadaj pytanie albo podziel się przydatną opinią.

Ładowanie...
Do góry