Niedziela wieczorem, 26 lipca. Przez formularz zgłoszeniowy wpada infekcja: strona klienta przekierowuje odwiedzających na obcą domenę, w panelu wisi konto administratora, którego nikt z zespołu nie zakładał. Klasyka przejęcia. Godzinę później drugie zgłoszenie, inny klient, ten sam obraz — świeży admin, podrzucony plugin, przekierowania. W poniedziałek doszły kolejne.

Niezależne strony przejęte w ten sam sposób w ciągu kilkunastu godzin to nie zbieg okoliczności. To fala. I ma nazwę: wp2shell — łańcuch dwóch podatności w core WordPressa, który pozwala nieuwierzytelnionemu atakującemu wykonać kod na serwerze. Bez logowania, bez żadnej wtyczki, na czystej instalacji.

CISA dopisała ją do listy aktywnie wykorzystywanych podatności 21 lipca. Publiczne PoC krążyły w sieci w ciągu godzin od ujawnienia. Od tamtej pory skanowanie i masowe przejęcia tylko przyspieszają — to, co opisuję wyżej, dzieje się teraz, na produkcyjnych stronach, w tym tygodniu.

Jeśli prowadzisz stronę na WordPressie 6.9 albo 7.0 i nie aktualizowałeś jej od zeszłego tygodnia, przestań czytać i zrób to teraz. Aktualizacja do 7.0.2, 6.9.5 lub 6.8.6 łata luki. Reszta tekstu poczeka — wyjaśnię w niej, na czym dokładnie polega ten błąd, dlaczego jest groźniejszy niż typowa podatność wtyczki, i co w tej historii jest naprawdę nowe: exploit, którego nie napisał człowiek.

Czym właściwie jest wp2shell?

Zacznijmy od tego, że „wp2shell” to nie jedna podatność, tylko nazwa nadana Å‚aÅ„cuchowi dwóch osobnych błędów, które dopiero razem prowadzÄ… do przejÄ™cia serwera:

  • CVE-2026-60137 — SQL injection w core WordPressa, w parametrze author__not_in klasy WP_Query. Samo w sobie pozwala czytać z bazy to, czego atakujÄ…cy nie powinien widzieć.
  • CVE-2026-63030 — „route confusion” w REST-owym endpoincie batch (/wp-json/batch/v1). To ten błąd zmienia SQL injection z uciążliwego wycieku danych w peÅ‚ne zdalne wykonanie kodu.

Każdy z osobna jest groźny. Połączone dajÄ… pre-auth RCE — trzy litery, które w Å›wiecie bezpieczeÅ„stwa oznaczajÄ… najgorszy możliwy scenariusz. „Pre-auth” znaczy, że atakujÄ…cy nie musi mieć żadnego konta ani danych logowania. „RCE” znaczy, że wykonuje wÅ‚asny kod na twoim serwerze. Bez klikniÄ™cia z twojej strony, bez zalogowanego użytkownika, bez wtyczki, którÄ… trzeba byÅ‚o zainstalować.

I tu jest sedno, dlaczego ta sprawa jest inna niż większość podatności, o których tu piszę. Zdecydowana większość CVE w ekosystemie WordPressa dotyczy wtyczek i motywów — kodu firm trzecich o różnej jakości. Podatną wtyczkę można wyłączyć, można jej nie mieć, można ją podmienić na lepszą.

wp2shell jest w core. W kodzie, który ma każda instalacja WordPressa — te 40-parÄ™ procent caÅ‚ego internetu. Nie ma tu wtyczki do odinstalowania ani feature’a do wyłączenia. JeÅ›li twoja wersja jest podatna, jesteÅ› podatny, kropka. Dlatego WordPress.org zrobiÅ‚ rzecz, którÄ… robi wyjÄ…tkowo rzadko: wymusiÅ‚ automatycznÄ… aktualizacjÄ™ nawet na stronach, które jÄ… majÄ… wyłączonÄ…. Skala i Å‚atwość eksploatacji nie zostawiÅ‚y innego wyjÅ›cia.

Tyle że wymuszona aktualizacja nie dociera do każdej strony samoistnie. Å»eby zadziaÅ‚aÅ‚a, musi siÄ™ faktycznie wykonać — a jest caÅ‚y zestaw powodów, dla których nie zawsze siÄ™ wykonuje. Mechanizm auto-update odpala siÄ™ przez wp-cron, wiÄ™c strona bez ruchu albo z wyłączonym cronem może godzinami nie dostać sygnaÅ‚u. Aktualizacja wymaga zapisu do plików core — jeÅ›li uprawnienia na to nie pozwalajÄ…, dysk jest peÅ‚ny albo hosting trzyma pliki core w trybie tylko do odczytu, proces cicho pada. A na wielu stronach ktoÅ› kiedyÅ› dopisaÅ‚ do wp-config.php AUTOMATIC_UPDATER_DISABLED albo DISALLOW_FILE_MODS — najczęściej „dla bezpieczeÅ„stwa” lub „żeby nic siÄ™ samo nie zmieniaÅ‚o” — i tym samym wypiÄ…Å‚ siÄ™ z dokÅ‚adnie tego mechanizmu, który miaÅ‚ go teraz uratować.

Jest jeszcze jedna rzecz, o której łatwo zapomnieć: aktualizacja łata podatność, ale nie usuwa skutków wcześniejszego włamania. Jeśli atakujący dostał się na stronę, zanim patch dojechał — a przy tempie tej fali okno było wąskie — to konto admina, backdoorowy plugin i przekierowania zostają na miejscu po aktualizacji. Zaktualizowana strona potrafi być jednocześnie zazałatana i wciąż przejęta. Dokładnie takie przypadki wpadały do mnie w poniedziałek.

Na czym polega ten błąd? Część pierwsza: SQL injection

Zacznijmy od dolnej warstwy łańcucha, czyli CVE-2026-60137. Żeby zrozumieć ten błąd, trzeba wiedzieć, czym jest author__not_in.

To caÅ‚kowicie legalny, udokumentowany parametr WP_Query — klasy, przez którÄ… WordPress odpytuje bazÄ™ praktycznie przy każdym Å‚adowaniu strony. Znaczy tyle co „daj mi wpisy, ale pomiÅ„ tych autorów”. Normalnie używasz go tak:

new WP_Query( array(
    'author__not_in' => array( 4, 7 ),
) );

WordPress bierze tę tablicę, rzutuje każdy element na liczbę całkowitą i wkleja do zapytania. Rzutowanie na int to darmowa ochrona przed SQL injection — cokolwiek atakujący by tam wcisnął, po (int) zostaje goła liczba. Efekt to bezpieczny kawałek SQL:

AND wp_posts.post_author NOT IN (4,7)

Fragmenty SQL w tym tekście to rekonstrukcja pokazująca mechanizm, nie dosłowne listingi z kodu core.

Problem pojawia się, gdy wartość przychodzi nie jako tablica, tylko jako pojedynczy string. Kod, który miał obsłużyć listę ID-ków, przy skalarnej wartości gubi krok rzutowania i wstawia ją do zapytania bez zmian. Atakujący, który kontroluje ten string, kontroluje fragment surowego SQL-a. To jest cała podatność w jednym zdaniu: parametr, który miał być listą liczb, staje się otwartym wejściem do zapytania.

Co to daje w praktyce? Odczyt tego, czego w bazie widzieć nie powinien — hashy haseł z wp_users, kluczy i tokenów, zawartości dowolnej tabeli. To klasyczny blind SQL injection: atakujący nie dostaje wyniku wprost, tylko wyciąga dane bit po bicie, obserwując, czy zapytanie zwróciło prawdę, czy jak długo trwało. Wolne, ale w pełni zautomatyzowane.

Jest tylko jeden haczyk — i to on przez chwilę trzymał tę podatność w ryzach. Żeby author__not_in trafiło do WP_Query jako skalarny string, trzeba je tam najpierw przemycić. A na czystej instalacji REST API waliduje ten parametr i oczekuje tablicy liczb, nie stringa. Samo SQL injection to zamknięte drzwi z dobrym zamkiem. Do ich otwarcia potrzebny jest drugi błąd — i tu wchodzi batch endpoint.

Na czym polega ten błąd? Część druga: batch route confusion

Batch endpoint jest w core WordPressa od wersji 5.6 (2020). Pozwala spakować kilka wywołań REST API w jedno żądanie do /wp-json/batch/v1 — zamiast wysyłać pięć osobnych żądań do pięciu endpointów, wysyłasz jeden request z listą pięciu podżądań. Jest dostępny bez logowania, bo to publiczny endpoint REST API. Można go wywołać nawet przy wyłączonych ładnych linkach, przez /?rest_route=/batch/v1.

Jak WordPress obsługuje taką paczkę? W dwóch osobnych przebiegach. Najpierw pętla walidacji przechodzi po wszystkich podżądaniach i sprawdza, czy każde jest poprawne. Potem druga pętla, wykonania, odpala je po kolei. Żeby oba przebiegi wiedziały, które podżądanie jest które, kod trzyma trzy równoległe listy indeksowane tą samą pozycją: $requests (co przyszło), $validation (wynik sprawdzenia) i $matches (do jakiego endpointu i handlera pasuje).

Cała sztuczka polega na rozjechaniu tych list o jeden.

Gdy w pętli walidacji trafi się podżądanie ze ścieżką na tyle nieprawidłową, że wp_parse_url() jej nie przetworzy, kod zapisuje błąd do $validation, ale robi continue i pomija dopisanie do $matches. Od tego momentu $matches jest o jeden element krótsza niż reszta. Indeksy przestają się zgadzać.

Efekt: w pętli wykonania każde podżądanie po tym celowo zepsutym jest dopasowane do endpointu i handlera należących do następnego w kolejce. Żądanie N wykonuje się pod handlerem żądania N+1.

To brzmi jak drobny off-by-one, a jest kluczem do całości, bo psuje dwie rzeczy naraz:

  • Uprawnienia. permission_callback, czyli bramka „czy ci wolno”, jest sprawdzana dla handlera, który nie należy do tego żądania. Nieszkodliwe, publiczne podżądanie przechodzi przez bramkÄ™ uprzywilejowanego endpointu. Tak wÅ‚aÅ›nie „pre-auth” dosiÄ™ga endpointów, które normalnie wymagajÄ… zalogowanego użytkownika.
  • WalidacjÄ™ parametrów. Sanityzacja argumentów też należy do zÅ‚ego endpointu. Ten sam mechanizm, który miaÅ‚ odrzucić skalarny author_exclude i zażądać tablicy liczb, po prostu nie jest już tym, który patrzy na ten parametr.

Do tego dochodzi jeszcze jeden szczegół. Batch normalnie nie przepuszcza GET-ów, a samo wstrzyknięcie SQL akurat GET-a wymaga. Atak obchodzi to rekurencyjnie — zagnieżdżając batch w batchu, tak że pole metody wewnętrznego żądania nigdy nie przechodzi walidacji i GET dojeżdża do /wp/v2/posts.

Zbierzmy to razem. Anonimowe żądanie na /wp-json/batch/v1 przemyca skalarny string do author_exclude, omija sanityzację, która trzymała SQL injection w ryzach, i wpuszcza go prosto do author__not_in w WP_Query. Zamknięte drzwi z części pierwszej stoją otworem. Payload w rodzaju 0) OR 1=1 -- potwierdza, że wstrzyknięcie działa.

Jak z odczytu bazy robi się przejęcie serwera?

W tym miejscu ktoś słusznie zapyta: dobrze, SQL injection pozwala czytać bazę, ale skąd nagle wykonanie kodu? Odczyt to nie zapis, a już na pewno nie uruchomienie własnego PHP. I to jest najciekawsza część tej historii — bo droga od blind SQLi do webshella wiedzie przez kilka najzupełniej legalnych mechanizmów WordPressa, poskładanych w nieprzewidzianej kolejności.

Pomaga tu druga własność błędu batcha z poprzedniej sekcji: obejście permission_callback samo z siebie otwiera atakującemu uprzywilejowane endpointy. Odczyt z SQLi dokłada do tego dane, których normalnie by nie zdobył. Reszta to kilka najzupełniej legalnych mechanizmów WordPressa, poskładanych w jedną całość. Według opisu badacza wygląda ona tak:

  • UNION-forged post — atakujÄ…cy używa UNION w injection, żeby WP_Query zwróciÅ‚o spreparowany wiersz „wpisu”, którego w bazie realnie nie ma, ale który wyglÄ…da dla WordPressa na prawdziwy.
  • Zatrucie cache oEmbed — pod ten spreparowany kontekst podkÅ‚adane sÄ… wÅ‚asne dane w cache oEmbed, czyli tam, gdzie WordPress siÄ™gnie po nie później jako po zaufane.
  • customize_changeset — spreparowany changeset Customizera pozwala atakujÄ…cemu chwilowo dziaÅ‚ać z uprawnieniami administratora.
  • Konto admina — majÄ…c te uprawnienia, tworzy realne, trwaÅ‚e konto administratora.
  • Wtyczka-webshell — loguje siÄ™ na nie i wgrywa wtyczkÄ™ z webshellem. To już zwykÅ‚e, „legalne” wgranie pluginu przez panel — i jednoczeÅ›nie moment, w którym atakujÄ…cy wykonuje dowolny kod PHP na serwerze.

I stąd właśnie te zgłoszenia z niedzieli: świeże konto admina, podrzucony plugin, przekierowania. Nie były przypadkowym wandalizmem — to końcówka dokładnie tego łańcucha, zautomatyzowana i puszczona masowo.

Kogo to dotyczy naprawdÄ™?

Tu jest niuans, który w wiÄ™kszoÅ›ci nagłówków ginie, a ma realne znaczenie dla oceny ryzyka. „wp2shell dotyczy 6.8, 6.9 i 7.0″ to skrót, który miesza dwie różne rzeczy.

Sama podatność SQL injection (CVE-2026-60137) w WP_Query jest obecna już od gałęzi 6.8. Batch route confusion (CVE-2026-63030), czyli mechanizm, który czyni z niej pre-auth RCE, pojawił się dopiero w 6.9. To rozróżnienie zmienia obraz ryzyka:

  • 6.9.0–6.9.4 i 7.0.0–7.0.1 — peÅ‚ny Å‚aÅ„cuch. Oba błędy obecne, nieuwierzytelnione RCE dziaÅ‚a. To sÄ… wersje, na których dzieje siÄ™ ta fala.
  • 6.8.0–6.8.5 — obecna jest tylko sama podatność SQLi. Bez batcha nie ma jej jak nakarmić skalarnym stringiem z anonimowego żądania na czystej instalacji. WordPress i tak zaÅ‚ataÅ‚ jÄ… defensywnie w 6.8.6 — bo wystarczy jedna wtyczka podajÄ…ca do tego parametru niewÅ‚aÅ›ciwy typ, żeby zrobiÅ‚o siÄ™ gorÄ…co.
  • Przed 6.8 — nietkniÄ™te.

Fix jest w 6.8.6, 6.9.5 i 7.0.2. Jeśli testowałeś betę 7.1, właściwa wersja to Beta 2.

Jak sprawdzić, jaką masz wersję? Najszybciej wp core version przez wp-cli, ewentualnie stopka panelu admina albo pole Version w readme.html w katalogu głównym (o ile nie zostało usunięte). Jeśli widzisz cokolwiek z zakresu 6.9.0–7.0.1, należysz do grupy pełnego ryzyka i aktualizacja nie może czekać.

Real talk: nawet jeÅ›li używasz wersji 6.8 i „technicznie” peÅ‚ny Å‚aÅ„cuch ciÄ™ nie dotyczy, to nie jest argument, żeby przy niej pozostać. Zostajesz dwie gałęzie w tyle, z niezaÅ‚atanÄ… podatnoÅ›ciÄ… SQLi w Å›rodku. Zaktualizuj do 7.0.2.

Exploit, którego nie napisał człowiek

Najciekawsze w tej całej historii nie jest samo RCE. Podatności w core zdarzały się wcześniej i zdarzą znowu. Nowe jest to, kto — a właściwie co — złożyło ten exploit.

Batch route confusion i peÅ‚ny, uzbrojony Å‚aÅ„cuch to praca Adama Kuesa z Assetnote/Searchlight Cyber. Ale nie w pojedynkÄ™: narzÄ™dziem byÅ‚ model jÄ™zykowy, GPT-5.6 „Sol”. WedÅ‚ug relacji badacza to model, przez rekurencyjne wywoÅ‚ania batcha, wyszedÅ‚ na podatny author_exclude i poskÅ‚adaÅ‚ całą sekwencjÄ™ post-exploitation — tÄ™ samÄ…, którÄ… rozebraliÅ›my wyżej, z UNION-forged postem, cache oEmbed i changesetem Customizera włącznie.

Liczby, które obiegły branżę: około 10 godzin i jakieś 25 dolarów w tokenach, mniej więcej połowa tygodniowego budżetu badacza. Tyle kosztowało zbudowanie działającego pre-auth RCE na kod, który obsługuje 40 procent internetu.

Zanim jednak ogÅ‚osimy, że maszyny piszÄ… już exploity same — precyzja. To nie byÅ‚ scenariusz „AI znalazÅ‚o 0-day, czÅ‚owiek tylko patrzyÅ‚”. Kues to doÅ›wiadczony badacz, a Assetnote to zespół z dorobkiem w dokÅ‚adnie takich Å‚aÅ„cuchach. Model byÅ‚ prowadzony: ktoÅ› stawiaÅ‚ hipotezy, weryfikowaÅ‚ wyniki, odrzucaÅ‚ Å›lepe uliczki i odpowiadaÅ‚ za responsible disclosure. Co wiÄ™cej, druga poÅ‚owa Å‚aÅ„cucha — samo SQL injection — zostaÅ‚a zgÅ‚oszona niezależnie przez trzech innych badaczy: TF1T, dtro i haongo. Ludzi. „Exploit, którego nie napisaÅ‚ czÅ‚owiek” to wiÄ™c skrót myÅ›lowy. ÅšciÅ›lej: exploit, w którym najżmudniejszÄ…, kreatywnÄ… robotÄ™ poskÅ‚adania tych mechanizmów w jeden Å‚aÅ„cuch wykonaÅ‚ model, kierowany przez eksperta.

Ale nawet z tym zastrzeżeniem to jest zmiana jakościowa, nie kosmetyczna. Co realnie się zmienia:

  • Koszt i czas budowy spadajÄ… do poziomu weekendowego projektu. To, co kiedyÅ› byÅ‚o tygodniami pracy specjalisty, mieÅ›ci siÄ™ w 10 godzinach i 25 dolarach.
  • Okno miÄ™dzy patchem a masowÄ… eksploatacjÄ… siÄ™ kurczy. Przy wp2shell publiczne PoC pojawiÅ‚y siÄ™ w godziny od ujawnienia. Im taÅ„sze uzbrojenie podatnoÅ›ci, tym mniej czasu masz na aktualizacjÄ™.
  • Model nie mÄ™czy siÄ™ przeszukiwaniem. PowiÄ…zanie kilku niepozornych mechanizmów, które czÅ‚owiek Å‚atwo przeoczy, bo trzeba naraz utrzymać w gÅ‚owie UNION-forged post, cache oEmbed i changeset, maszyna wypracowuje cierpliwie i systematycznie.

Dla ciebie jako wÅ‚aÅ›ciciela albo administratora strony pÅ‚ynie z tego prosty, choć niewygodny wniosek. Popularny model „opieki” nad stronÄ… — aktualizacje wedÅ‚ug staÅ‚ego harmonogramu, powiedzmy dwa razy w miesiÄ…cu — zakÅ‚adaÅ‚, że miÄ™dzy wyjÅ›ciem patcha a realnym zagrożeniem masz zapas czasu. Przy wp2shell tego zapasu nie byÅ‚o: PoC krążyÅ‚y w godziny, fala ruszyÅ‚a w dni. Strona aktualizowana „w najbliższym oknie serwisowym” mogÅ‚a zostać przejÄ™ta na dÅ‚ugo przed tym oknem.

Nie chodzi o to, żeby wyrzucić harmonogram do kosza — regularne aktualizacje to wciąż podstawa. Chodzi o to, żeby obok niego istniała druga ścieżka: krytyczne łatki poza kolejnością, w godzinach, nie w najbliższym wolnym terminie. A to wymaga rzeczy, o której łatwo zapomnieć — stałego monitoringu podatności. Żeby zaktualizować szybko, najpierw trzeba w ogóle wiedzieć, że jest co aktualizować. wp2shell trafiło na listę KEV w kilka dni, ale strona bez kogoś — albo czegoś — kto śledzi takie sygnały, dowiaduje się o problemie dopiero z przekierowań na obcą domenę. Jeśli twoja umowa serwisowa nie obejmuje ani szybkiej ścieżki na krytyczne łatki, ani monitoringu, który ją uruchamia, wp2shell jest dobrym pretekstem, żeby o to dopytać. O tym, dlaczego samo zwlekanie z aktualizacjami tak regularnie kończy się przejęciem, pisałem osobno — Aktualizować, zwlekać, ignorować?

Co z tym zrobić?

Jeśli dobrnąłeś tutaj, a nie zaktualizowałeś jeszcze na początku — zrób to teraz. Reszta tej sekcji zakłada, że wersja jest już bezpieczna, i mówi o tym, co dalej: jak sprawdzić, czy nie jest za późno.

1. Potwierdź wersję i aktualizację. wp core version powinno pokazać 7.0.2, 6.9.5 albo 6.8.6. Jeśli auto-update nie zadziałał z powodów, które opisałem wcześniej, zaktualizuj ręcznie — z panelu albo wp core update.

2. Sprawdź logi pod kątem prób. Ślad ataku jest charakterystyczny: żądania na /wp-json/batch/v1 albo /?rest_route=/batch/v1, często metodą POST, z zagnieżdżoną strukturą batcha w treści. W logach dostępowych szukaj tych ścieżek — pojedyncze trafienia to zwykle skanery, ale to sygnał, że jesteś na celowniku.

3. Sprawdź, czy cię nie przejęto. To jest ważniejsze niż same logi, bo aktualizacja niczego nie cofa. Konkretnie:

  • Nowe konta z rolÄ… administratora, których nikt nie zakÅ‚adaÅ‚ — wp user list --role=administrator.
  • Åšwieże albo nieznane wtyczki, zwÅ‚aszcza takie, których nie ma w repozytorium WordPressa — wp plugin list.
  • Nieoczekiwane pliki PHP w wp-content/uploads/ i przekierowania w .htaccess albo w treÅ›ci strony.

Jeśli którykolwiek z tych punktów wypadł podejrzanie, przyjmij, że strona jest przejęta, i traktuj to jak każdą inną infekcję: czyszczenie z backupu sprzed kompromitacji, nie ręczne usuwanie tego, co widać. O tym, dlaczego niezależny backup jest tu wart tyle, ile cała reszta zabezpieczeń razem, pisałem osobno — Pożar w serwerowni, czyli na co mi niezależny backup. A jak w praktyce wygląda rozkładanie takiej infekcji na czynniki pierwsze, pokazywałem tutaj — Infekcja letsmakeparty3 / sdfsd234.

Wnioski

wp2shell zapamiętamy z dwóch powodów, i tylko jeden z nich dotyczy WordPressa.

Ten pierwszy: pre-auth RCE w core, na czystej instalacji, bez wtyczek — to najgorszy scenariusz, jaki może spotkać CMS, i przypomnienie, że REST API jest pełnoprawną powierzchnią ataku, nie dodatkiem. Batch endpoint był w kodzie od 2020 roku i przez pięć lat nikt nie zauważył, że rozjeżdżają się w nim trzy równoległe listy. To nie jest zarzut wobec WordPressa — to natura złożonego kodu, w którym dwa niewinne błędy w dwóch różnych miejscach dopiero razem tworzą katastrofę.

Ten drugi jest większy niż WordPress. Exploit złożył model językowy w dziesięć godzin za dwadzieścia pięć dolarów. Można się spierać, ile w tym zasługi maszyny, a ile prowadzącego ją człowieka — i słusznie, bo ten balans ma znaczenie. Ale kierunek jest jasny: uzbrajanie podatności właśnie stało się o rząd wielkości tańsze i szybsze. Obrona po drugiej stronie musi za tym nadążyć — a to znaczy krótsze okna na aktualizację, monitoring, który wyłapuje krytyczne CVE w dniach, nie tygodniach, i backup, który działa, zanim będzie potrzebny.

Zaktualizuj do 7.0.2. A potem sprawdź, czy nie jesteś już po fakcie.

Change consents