8.0 KiB
Java SignedObject-gated Deserialization i Pre-auth Reachability putem Error Paths
{{#include ../../banners/hacktricks-training.md}}
Ova stranica dokumentuje uobičajeni "zaštićeni" Java deserialization obrazac zasnovan na java.security.SignedObject i kako naizgled nedostupni sinkovi mogu postati pre-auth dostupni putem tokova obrade grešaka. Tehnika je primećena u Fortra GoAnywhere MFT (CVE-2025-10035) ali je primenjiva na slične dizajne.
Model pretnje
- Napadač može da dosegne HTTP endpoint koji na kraju obrađuje attacker-supplied byte[] namenjen da bude serializovan SignedObject.
- Kod koristi validating wrapper (npr. Apache Commons IO ValidatingObjectInputStream ili custom adapter) da ograniči najspoljni tip na SignedObject (ili byte[]).
- Unutrašnji objekat koji vraća SignedObject.getObject() je mesto gde gadget lanci mogu da se okinu (npr. CommonsBeanutils1), ali samo nakon gate-a za verifikaciju potpisa.
Tipičan ranjiv obrazac
Pojednostavljen primer zasnovan na com.linoma.license.gen2.BundleWorker.verify:
private static byte[] verify(byte[] payload, KeyConfig keyCfg) throws Exception {
String sigAlg = "SHA1withDSA";
if ("2".equals(keyCfg.getVersion())) {
sigAlg = "SHA512withRSA"; // key version controls algorithm
}
PublicKey pub = getPublicKey(keyCfg);
Signature sig = Signature.getInstance(sigAlg);
// 1) Outer, "guarded" deserialization restricted to SignedObject
SignedObject so = (SignedObject) JavaSerializationUtilities.deserialize(
payload, SignedObject.class, new Class[]{ byte[].class });
if (keyCfg.isServer()) {
// Hardened server path
return ((SignedContainer) JavaSerializationUtilities.deserializeUntrustedSignedObject(
so, SignedContainer.class, new Class[]{ byte[].class }
)).getData();
} else {
// 2) Signature check using a baked-in public key
if (!so.verify(pub, sig)) {
throw new IOException("Unable to verify signature!");
}
// 3) Inner object deserialization (potential gadget execution)
SignedContainer inner = (SignedContainer) so.getObject();
return inner.getData();
}
}
Ključna zapažanja:
- Validirajući deserializer na (1) blokira proizvoljne top-level gadget classes; prihvatan je samo SignedObject (ili raw byte[]).
- RCE primitive bi bio u unutrašnjem objektu materializovanom pozivom SignedObject.getObject() na (3).
- A signature gate na (2) zahteva da se SignedObject verifikuje protiv ugrađenog javnog ključa proizvoda. Ako napadač ne može da proizvede validan potpis, unutrašnji gadget se nikad ne deserijalizuje.
Razmatranja za eksploataciju
Da bi se postiglo izvršavanje koda, napadač mora dostaviti ispravno potpisan SignedObject koji obavija maliciozni gadget chain kao svoj unutrašnji objekat. To generalno zahteva jednu od sledećih opcija:
- Kompromitovanje privatnog ključa: pribaviti odgovarajući privatni ključ koji proizvod koristi za potpis/verifikaciju objekata licence.
- Signing oracle: prisiliti vendor-a ili pouzdanu signing service da potpiše serialized sadržaj pod kontrolom napadača (npr. ako license server potpisuje ugrađeni proizvoljan objekat iz client input).
- Alternate reachable path: pronaći server-side putanju koja deserijalizuje unutrašnji objekat bez pozivanja verify(), ili koja preskače provere potpisa u specifičnom režimu.
U odsustvu jedne od ovih opcija, verifikacija potpisa će sprečiti eksploataciju uprkos postojanju deserialization sink-a.
Pre-auth dostižnost putem tokova obrade grešaka
Čak i kada endpoint za deserijalizaciju deluje kao da zahteva autentikaciju ili token vezan za sesiju, kod koji obrađuje greške može nenamerno generisati i prikačiti token na neautentifikovanu sesiju.
Primer lanca dostižnosti (GoAnywhere MFT):
- Target servlet: /goanywhere/lic/accept/ zahteva session-bound license request token.
- Error path: pristupanje /goanywhere/license/Unlicensed.xhtml sa dodatnim junk podacima i nevalidnim JSF state-om pokreće AdminErrorHandlerServlet, koji radi:
- SessionUtilities.generateLicenseRequestToken(session)
- Redirects to vendor license server with a signed license request in bundle=<...>
- Bundle se može dekriptovati offline (hard-coded keys) kako bi se rekonstruisao GUID. Zadržati isti session cookie i POST-ovati na /goanywhere/lic/accept/ sa attacker-controlled bundle bajtovima, dostižući SignedObject sink pre-auth.
Proof-of-reachability (impact-less) probe:
GET /goanywhere/license/Unlicensed.xhtml/x?javax.faces.ViewState=x&GARequestAction=activate HTTP/1.1
Host: <target>
- Nezakrpljeno: 302 Location header ka https://my.goanywhere.com/lic/request?bundle=... i Set-Cookie: ASESSIONID=...
- Zakrpljeno: preusmeravanje bez bundle-a (nema generisanja tokena).
Detekcija Blue tima
Indikatori u stack traces/logs snažno ukazuju na pokušaje da se pogodi SignedObject-gated sink:
java.io.ObjectInputStream.readObject
java.security.SignedObject.getObject
com.linoma.license.gen2.BundleWorker.verify
com.linoma.license.gen2.BundleWorker.unbundle
com.linoma.license.gen2.LicenseController.getResponse
com.linoma.license.gen2.LicenseAPI.getResponse
com.linoma.ga.ui.admin.servlet.LicenseResponseServlet.doPost
Smernice za ojačavanje bezbednosti
- Održavajte verifikaciju potpisa pre bilo kog getObject() poziva i osigurajte da verifikacija koristi predviđeni javni ključ/algoritam.
- Zamenite direktne SignedObject.getObject() pozive hardenovanim wrapperom koji ponovo primenjuje filtriranje na unutrašnji stream (npr. deserializeUntrustedSignedObject koristeći ValidatingObjectInputStream/ObjectInputFilter i liste dozvoljenih).
- Uklonite tokove u error-handlerima koji izdaju tokene vezane za sesiju neautentifikovanim korisnicima. Tretirajte puteve grešaka kao površinu napada.
- Dajte prednost Java serialization filters (JEP 290) sa strogim listama dozvoljenih tipova za i spoljne i unutrašnje deserializacije. Primer:
ObjectInputFilter filter = info -> {
Class<?> c = info.serialClass();
if (c == null) return ObjectInputFilter.Status.UNDECIDED;
if (c == java.security.SignedObject.class || c == byte[].class) return ObjectInputFilter.Status.ALLOWED;
return ObjectInputFilter.Status.REJECTED; // outer layer
};
ObjectInputFilter.Config.setSerialFilter(filter);
// For the inner object, apply a separate strict DTO allow-list
Primer rekapitulacije lanca napada (CVE-2025-10035)
- Pre-auth mintovanje tokena putem error handler-a:
GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate
Primite 302 sa bundle=... i ASESSIONID=...; dešifrujte bundle offline da biste povratili GUID.
- Pristupite sink pre-auth koristeći isti cookie:
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded
bundle=<attacker-controlled-bytes>
- RCE zahteva ispravno potpisan SignedObject koji obavija gadget chain. Istraživači nisu uspeli da zaobiđu verifikaciju potpisa; eksploatacija zavisi od pristupa odgovarajućem private key ili signing oracle.
Ispravljene verzije i promene ponašanja
- GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:
- Harden inner deserialization by replacing SignedObject.getObject() with a wrapper (deserializeUntrustedSignedObject).
- Uklonjena generacija tokena error-handler-a, čime je zatvorena pre-auth dostupnost.
Napomene o JSF/ViewState
Trik sa dostupnošću koristi JSF stranicu (.xhtml) i nevažeći javax.faces.ViewState da preusmeri u privilegovani handler za greške. Iako ovo nije JSF deserialization problem, to je ponavljajući pre-auth obrazac: probijanje u error handlere koji izvode privilegovane akcije i postavljaju bezbednosno-relevantne atribute sesije.
References
- watchTowr Labs – Is This Bad? This Feels Bad — GoAnywhere CVE-2025-10035
- Fortra advisory FI-2025-012 – Deserialization Vulnerability in GoAnywhere MFT's License Servlet
{{#include ../../banners/hacktricks-training.md}}