Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths
Reading time: 7 minutes
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Questa pagina documenta un comune pattern di deserializzazione "protetto" in Java basato su java.security.SignedObject e come sink apparentemente non raggiungibili possano diventare raggiungibili pre-auth tramite flussi di gestione degli errori. La tecnica è stata osservata in Fortra GoAnywhere MFT (CVE-2025-10035) ma è applicabile a design simili.
Modello di minaccia
- Un attaccante può raggiungere un endpoint HTTP che alla fine elabora un byte[] fornito dall'attaccante e destinato a essere un SignedObject serializzato.
- Il codice usa un wrapper di validazione (es., Apache Commons IO ValidatingObjectInputStream o un adattatore personalizzato) per vincolare il tipo esterno a SignedObject (o byte[]).
- L'oggetto interno restituito da SignedObject.getObject() è il punto in cui le gadget chain possono attivarsi (es., CommonsBeanutils1), ma solo dopo un controllo di verifica della firma.
Pattern vulnerabile tipico
Un esempio semplificato basato su 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();
}
}
Osservazioni chiave:
- Il deserializer di validazione in (1) blocca arbitrary top-level gadget classes; solo SignedObject (o raw byte[]) è accettato.
- La primitiva RCE risiederebbe nell'oggetto interno materializzato da SignedObject.getObject() in (3).
- Un controllo di firma in (2) impone che il SignedObject debba verificare contro una chiave pubblica incorporata nel prodotto. A meno che l'attaccante non possa produrre una firma valida, il gadget interno non viene mai deserializzato.
Considerazioni sull'exploit
Per ottenere l'esecuzione di codice, un attaccante deve consegnare un SignedObject correttamente firmato che avvolge una catena di gadget malevoli come oggetto interno. Questo generalmente richiede una delle seguenti condizioni:
- Compromissione della chiave privata: ottenere la chiave privata corrispondente usata dal prodotto per firmare/verificare gli oggetti di licenza.
- Signing oracle: costringere il vendor o un servizio di firma di fiducia a firmare contenuti serializzati controllati dall'attaccante (es., se un license server firma un oggetto arbitrario incorporato dall'input client).
- Percorso alternativo raggiungibile: trovare un percorso server-side che deserializzi l'oggetto interno senza applicare verify(), o che salti i controlli della firma in una modalità specifica.
In assenza di una di queste condizioni, la verifica della firma impedirà lo sfruttamento nonostante la presenza di un deserialization sink.
Raggiungibilità pre-auth tramite flussi di gestione degli errori
Anche quando un endpoint di deserializzazione sembra richiedere autenticazione o un token legato alla sessione, il codice di gestione degli errori può involontariamente creare e allegare il token a una sessione non autenticata.
Esempio di catena di raggiungibilità (GoAnywhere MFT):
- Servlet target: /goanywhere/lic/accept/
richiede un token di richiesta licenza legato alla sessione. - Percorso di errore: colpire /goanywhere/license/Unlicensed.xhtml con trailing junk e stato JSF invalido scatena AdminErrorHandlerServlet, che esegue:
- SessionUtilities.generateLicenseRequestToken(session)
- Effettua redirect al vendor license server con una richiesta di licenza firmata in bundle=<...>
- Il bundle può essere decrittato offline (chiavi hard-coded) per recuperare il GUID. Conservare lo stesso cookie di sessione e fare POST a /goanywhere/lic/accept/
con bundle bytes controllati dall'attaccante, raggiungendo il 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>
- Non patchato: 302 Location header verso https://my.goanywhere.com/lic/request?bundle=... e Set-Cookie: ASESSIONID=...
- Corretto: reindirizzamento senza bundle (nessuna generazione di token).
Rilevamento Blue-team
Indicatori negli stack traces/logs suggeriscono fortemente tentativi di colpire uno 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
Linee guida per l'hardening
- Mantenere la verifica della firma prima di qualsiasi chiamata a getObject() e assicurarsi che la verifica utilizzi la chiave pubblica/algoritmo previsto.
- Sostituire le chiamate dirette a SignedObject.getObject() con un wrapper rinforzato che riapplica il filtraggio allo stream interno (es., deserializeUntrustedSignedObject usando ValidatingObjectInputStream/ObjectInputFilter allow-lists).
- Rimuovere i flussi dei gestori di errore che emettono token legati alla sessione per utenti non autenticati. Considerare i percorsi di errore come superficie d'attacco.
- Preferire i Java serialization filters (JEP 290) con allow-lists rigorose sia per la deserializzazione esterna che per quella interna. Esempio:
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
Riepilogo della catena d'attacco di esempio (CVE-2025-10035)
- Pre-auth token minting tramite l'error handler:
GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate
Ricevi un 302 con bundle=... e ASESSIONID=...; decripta il bundle offline per recuperare il GUID.
- Raggiungi il sink pre-auth con lo stesso cookie:
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded
bundle=<attacker-controlled-bytes>
- RCE richiede un SignedObject correttamente firmato che incapsuli una gadget chain. I ricercatori non sono riusciti a bypassare la verifica della firma; lo sfruttamento dipende dall'accesso a una chiave privata corrispondente o a un signing oracle.
Versioni corrette e cambiamenti comportamentali
- GoAnywhere MFT 7.8.4 e Sustain Release 7.6.3:
- Rinforzare la deserializzazione interna sostituendo SignedObject.getObject() con un wrapper (deserializeUntrustedSignedObject).
- Rimuovere la generazione del token dell'error-handler, chiudendo la raggiungibilità pre-auth.
Note su JSF/ViewState
Il trucco della raggiungibilità sfrutta una pagina JSF (.xhtml) e un javax.faces.ViewState non valido per indirizzare verso un error handler privilegiato. Pur non essendo un problema di deserializzazione JSF, è un pattern ricorrente pre-auth: entrare negli error handler che eseguono azioni privilegiate e impostano attributi di sessione rilevanti per la sicurezza.
Riferimenti
- 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
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.