
Reprodutível prova de conceito baseada em Docker para CVE-2026-19949, uma injeção SQL de segunda ordem no All-in-One WP Migration <= 7.109 que vaza a ai1wm_secret_key via REST anônimo e escala para execução remota de código.
Injeção SQL de segunda ordem não autenticada em All-in-One WP Migration and Backup (WordPress)
que escala para vazamento da ai1wm_secret_key e, com ela, para execução remota de código (RCE).
Leia isto em: English · Português
| CVE | CVE-2026-19949 |
| Plugin | All-in-One WP Migration and Backup (ServMask), ≤ 7.109 |
| Correção | 7.110 (20 de agosto de 2026) |
| CVSS | 8.8 (Alta) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Requisito | Que um administrador exporte e restaure o site (ação rotineira com este plugin) |
| Pesquisador | Jack Taylor (programa de recompensas da Wordfence) |
⚠️ Uso exclusivamente educacional e defensivo. Este laboratório ataca um site WordPress que roda na sua própria máquina, dentro de contêineres Docker. Não o use contra sistemas que não lhe pertençam ou para os quais você não tenha autorização explícita.
O fluxo de exportação/importação do plugin despeja o banco de dados em SQL
(database.sql dentro do .wpress) e, ao importar, reescreve cada instrução
com Ai1wm_Database::replace_table_values() para substituir URLs e prefixos de
tabela. Para localizar os literais de string, usa o regex:```php
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
O problema está no *negative lookbehind* `(?<!\\)`: **só examina um byte**
antes de cada aspas candidata a fechamento, em vez de contar o *run* completo de
barras invertidas. Em um dump MySQL, dados que terminam em `\` são gravados como
`...\\'` — uma aspa de fechamento precedida de um número **par** de barras invertidas
(string real terminada em barra invertida) — mas o regex a considera escapada e
**sobre-captura o próximo literal**. O callback executa então
`unescape_mysql → replace_serialized_values → escape_mysql` sobre o valor
sobre-capturado, e esse ciclo reemite uma sequência de aspas/barras invertidas
desequilibrada que **inverte o limite da string MySQL** na instrução
resultante, promovendo dados do atacante a SQL executável.
## 2. Causa raiz: o regex que inverte a string
Com o trackback plantado (ver §4), a linha em `database.sql` é:```sql
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
O author termina em \ a nível de dados → o dump o escreve como \\ →
a aspa de fechamento fica precedida de um run par. O regex sobre-captura
até a aspa de abertura do campo seguinte e o callback re-escapa o
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
A partir daí, todo o que seguia na sentença é re-tokenizado com a
paridade alterada (dados que eram string passam a ser código e vice-versa). O
regex corrigido do 7.110 processa a mesma linha e a devolve **idêntica**.
Você pode ver isso byte a byte, sem explorar nada, com `make demo-flip`
(`exploit/04_demo_flip.php`), que executa o código REAL do plugin 7.109 e
7.110 sobre a mesma linha de dump.
## 3. O patch (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 );
O novo padrão tokeniza literais MySQL corretamente: ou um caractere que não é
aspas/barra invertida, ou um par de escape \x, com quantificadores possessivos. Um par
de barras invertidas não confunde mais o fechamento do literal.
A cadeia publicada pela Wordfence (set. 2026) tem quatro etapas:
wp-trackback.php?p=<id>). O blog name
(→ comment_author) termina em \ e a URL (→ comment_author_url) carrega
o payload. O WordPress os armazena sem tocar na barra. Na variante
publicada, o primeiro trackback atua como bomba-relógio: força o corte
da passagem de importação (limite de 10 s) para que o payload seja executado em
uma passagem posterior, quando o importador já restaurou a
ai1wm_secret_key do site em wp_options (o exportador a exclui do
dump; o importador a reescreve entre passagens).ai1wm_secret_key para um comentário aprovado
do tipo comment, visível na REST API de comentários sem autenticação.admin-ajax.php?action=ai1wm_import também está registrado para
usuários anônimos (wp_ajax_nopriv_ai1wm_import) e sua única barreira é
ai1wm_verify_secret_key(). Com a chave filtrada, o atacante conduz a
cadeia de importação com seu próprio .wpress que inclui um mu-plugin;
a etapa Ai1wm_Import_Mu_Plugins (prioridade 270 da cadeia) o extrai para
wp-content/mu-plugins/ → execução de código no próximo carregamento.Este repositório verifica a cadeia de ponta a ponta com um único trackback plantado (a bomba-relógio não é necessária no laboratório: a linha é executada em uma passagem posterior ao restabelecimento da chave).
O exploit exato do pesquisador não é público (busca exaustiva em
2026-09-07: GitHub — os 2 repositórios existentes são um template vazio e uma
ferramenta massiva com endpoints inexistentes —, Exploit-DB/PacketStorm 0,
Sploitus só indexa esses repositórios falsos, WPScan sem PoC, fóruns só notícias;
detalhe forense em research/poc-publica/ANALISIS.md).
Nós o derivamos a partir da análise do mecanismo
(exploit/investigacion/). Design:
\ → o lookbehind de um byte do regex
vulnerável sobre-captura o literal seguinte e o ciclo unescape→escape
inverte o limite da string.comment_author mesclado e uma string , absorvem o
deslocamento; uma vírgula inicial na URL restaura o separador
estrutural devorado)./**/ como separadores:
\' e em zona de código deixaria
uma barra órfã (erro 1064) → daí o hex (0x616931776d… =
"ai1wm_secret_key");sanitize_url remove os espaços (e antepõe http:// a tudo que
não comece por /) → daí o início /*pwn*/ e os /**/;comment_author_url) = a subconsulta que lê
a chave → a chave acaba em um campo público; a 11 = 0x31
('1', comentário aprovado); a 13 = 0x636f6d6d656e74 ('comment',
visível na REST anônima);) fecha a tupla com exatamente 15 valores e # (comentário MySQL
sem espaço, que sanitize_url respeita) neutraliza o restante da
instrução original.SERVMASK_PREFIX_options: o importador
reescreve os prefixos SERVMASK→reais antes da passagem do regex, então
o payload funciona em sites com qualquer prefixo de tabela
(verificado). O único dado específico do alvo é sua URL, carregada no
excerpt para disparar o filtro strpos do importador (só reescreve
linhas que a contêm).A instrução resultante (a que o MySQL executa durante a restauração):
comment_author_url = (SELECT option_value FROM <prefijo>_options WHERE option_name='ai1wm_secret_key') — a chave autêntica fica em um campo que a
REST API publica sem autenticação.
| Etapa da cadeia | Estado | Onde |
|---|---|---|
| Instalação vulnerável reproduzível (WP 7.1 + plugin 7.109) | ✅ | make lab |
| Plantio não autenticado via trackback (byte-exato, auto-aprovado) | ✅ | make plant |
| Export do admin; o dump contém a linha plantada | ✅ | make export |
| Causa raiz: regex 7.109 inverte o limite da string (vs 7.110 intacto) | ✅ | make demo-flip |
| Restauração do admin: o flip reescreve o SQL e corrompe/perde a linha em 7.109 | ✅ | make restore |
| Filtragem da chave (payload derivado próprio) → leak real via REST sem autenticar | ✅ | make leak |
| RCE: import não autenticado com a chave → mu-plugin executado | ✅ | make rce-auto |
| Controle negativo: em 7.110 a linha sobrevive intacta, sem leak | ✅ | make control-7110 |
| Matriz WP 7.1 / 7.0.4 / 6.9.4 / 6.8.3: cadeia completa ✓ em todas | ✅ | research/test-wp-versions.sh |
| Cadeia completa remota (só HTTP, válida para um domínio real) | ✅ | python3 cli/poc.py explotar … |
Resultado verificado end-to-end: após o export+restore do admin, a
ai1wm_secret_key autêntica aparece como author_url de um comentário
aprovado em GET /wp-json/wp/v2/comments — sem autenticação alguma — e com
ela é executada a fase RCE (mu-plugin extraído e executado, marcadores de
evidência em wp-content/). Em 7.110 a linha sobrevive intacta (o payload
fica como dados inertes) e não há leak.
A fase de RCE é demonstrada também de forma independente: sua única
entrada é a chave, que pode ser passada manualmente (make rce KEY=…) — o
estado exato do atacante após o leak.
Matriz executada com bash research/test-wp-versions.sh 7.0 6.9 6.8
(laboratório reconstruído por versão, plugin 7.109, payload idêntico):
| WordPress | plant | export | restore | leak | RCE |
|---|---|---|---|---|---|
| 7.1.0 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7.0.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.9.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.8.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
A mecânica é estável em todas: sanitize_url respeita o payload,
wp_comments conserva as 15 colunas e wp-trackback.php segue operante.
O payload é independente do prefixo de tabela (ver §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 e **não são compartilhados** (ver `.gitignore`):
`informes/` (contêm chaves filtradas), `exploit/malicious.wpress`,
`plugin-src/` (fontes 7.109/7.110 extraídas para diff), `research/*.zip`
(zips originais do WordPress.org), `research/database.sql`,
`research/backup-legit.wpress` e os ficheiros das PoCs falsas analisadas.
## 9. Requisitos e arranque rápido
Requisitos: Docker (com o plugin compose), make, python3 com `requests`, curl.```bash
make full-demo # laboratorio completo de cero: lab → plant → export →
# restore → flip → leak → rce (todo lo anterior de una vez)
Salida esperada (resumida):``` 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
Acesso ao site vítima: <http://localhost:8080> — admin / `admin-password-123`
(entrada pública ID 4 com pings e comentários abertos).
## 10. Fases passo a passo (objetivos do 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
Um único comando com todos os modos; gera relatórios em informes/
(texto + JSON; a pasta está fora do repositório porque contém chaves):```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` levanta o laboratório, executa a cadeia completa e o controle
negativo com 7.110, e escreve o relatório (`--sem-controle` para omiti-lo).
- `scan` é 100 % passivo: lê o `readme.txt` público do plugin, verifica
`wp-trackback.php` e a REST de comentários. Retorna o veredito.
- `verificar` re-verifica se a chave já aparece na REST anônima (p. ex.
horas depois de plantar, quando o admin tiver feito seu backup+restore).
- `explotar` é INVASIVO e exige `--aceito-responsabilidade` + confirmação
interativa do domínio: planta o trackback, simula o admin
(export → download do backup → re-upload e restore; **somente HTTP**, com as
credenciais que VOCÊ fornecer do SEU site), lê a chave filtrada e, com
`--rce`, deixa um marcador benigno via importação anônima. Sem credenciais
de admin: planta e fica à espera (`--esperar` para aguentar o leak;
`--post-id` para escolher entrada; `--no-interativo` para scripts).
## 12. Landing do CVE
Página estática bilíngue (ES/EN, comutador no cabeçalho, preferência
lembrada) que explica o CVE e o payload com estética de banco de dados de
vulnerabilidades: resumo, cadeia, **detalhamento interativo do payload
segmento a segmento**, matrizes de verificação e mitigação.```bash
make landing # sirve http://localhost:8090
Sem dependências externas (nem CDN nem JS de terceiros): funciona abrindo
web/index.html diretamente ou com qualquer servidor estático.
wp-trackback.php e pingbacks
(opções de debate / WAF), restringir admin-ajax.php para anônimos quando
possível, e monitorar o aparecimento de arquivos em
wp-content/mu-plugins/ e da opção ai1wm_secret_key em contextos
anômalos (p. ex. comentários).comment_author termina em \ ou cujo comment_author_url contém
SELECT/**/, CONCAT(, literais 0x… longos ou /*…*/; comentários
aprovados cujo author_url é uma string alfanumérica de 12 caracteres sem
esquema.exploit/investigacion/): partimos de um harness
(harness.php) que carrega as classes REAIS do plugin 7.109 e executa o
pipeline contra um MySQL de teste com uma chave falsa. Os fuzzers
(search_payload.php, fuzz_rows.php, fuzz4.php,
bruteforce_author.php) exploraram alfabetos críticos sobre os quatro
campos controláveis do trackback; afinar_payload.php ajustou o número de
expressões (N=12) e test_payload_final.php validou o design completo.
NOTAS.md reúne a análise, incluindo os muros estruturais (campos
fixos do INSERT de wp_comments, sql_mode imposto pelo importador,
filtro strpos da URL do site).research/poc-publica/ANALISIS.md): os
dois repositórios do GitHub que afirmam tê-lo usam endpoints inexistentes
(aio-migration/v1 com permission_callback vs o ai1wm/v1 real), campo
de upload errado (file vs upload_file), formato ZIP em vez de
.wpress, e uma premissa circular. Nenhuma fonte terciária tem PoC
própria.lib/vendor/servmask/database/ class-ai1wm-database.php:1637; as fontes extraídas vivem em plugin-src/,
não compartilhadas)Este material é publicado para fins educacionais e defensivos: entender a
vulnerabilidade, verificar o patch e construir detecções. A cadeia só foi
executada contra laboratórios Docker próprios. Atacar sistemas de terceiros
sem autorização por escrito é ilegal na maioria das jurisdições. Se você
administra sites afetados: atualize para ≥ 7.110, rotacione a
ai1wm_secret_key (desative/reative o plugin ou apague a opção para que
ela seja regenerada) e audite wp-content/mu-plugins/ e os comentários
recentes.