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

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:

java
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:

http
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:
java
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)

  1. Pre-auth token minting tramite l'error handler:
http
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.

  1. Raggiungi il sink pre-auth con lo stesso cookie:
http
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded

bundle=<attacker-controlled-bytes>
  1. 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

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