Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths
Reading time: 7 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Cette page documente un motif courant de désérialisation Java « guarded » construit autour de java.security.SignedObject et comment des sinks apparemment inaccessibles peuvent devenir atteignables avant authentification via des flux de gestion d'erreurs. La technique a été observée dans Fortra GoAnywhere MFT (CVE-2025-10035) mais s'applique à des conceptions similaires.
Threat model
- Un attaquant peut atteindre un endpoint HTTP qui traite finalement un byte[] fourni par l'attaquant destiné à être un SignedObject sérialisé.
- Le code utilise un wrapper de validation (par ex., Apache Commons IO ValidatingObjectInputStream ou un adaptateur personnalisé) pour contraindre le type le plus externe à SignedObject (ou byte[]).
- L'objet interne renvoyé par SignedObject.getObject() est l'endroit où des gadget chains peuvent se déclencher (par ex., CommonsBeanutils1), mais seulement après une vérification de signature.
Typical vulnerable pattern
Un exemple simplifié basé sur 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();
}
}
Observations clés:
- Le deserializer de validation à (1) bloque les classes de gadget top-level arbitraires ; seul SignedObject (ou raw byte[]) est accepté.
- Le primitive RCE se trouverait dans l'objet interne matérialisé par SignedObject.getObject() à (3).
- Une signature gate à (2) impose que le SignedObject doit passer verify() contre une clé publique intégrée au produit. À moins que l'attaquant puisse produire une signature valide, le gadget interne n'est jamais désérialisé.
Considérations d'exploitation
Pour obtenir une exécution de code, un attaquant doit livrer un SignedObject correctement signé qui encapsule une gadget chain malveillante comme objet interne. Cela requiert en général l'un des éléments suivants :
- Private key compromise : obtenir la clé privée correspondante utilisée par le produit pour signer/vérifier les objets de licence.
- Signing oracle : contraindre le fournisseur ou un service de signature de confiance à signer du contenu sérialisé contrôlé par l'attaquant (par ex., si un license server signe un objet arbitraire embarqué provenant d'une entrée client).
- Alternate reachable path : trouver un chemin côté serveur qui désérialise l'objet interne sans appeler verify(), ou qui saute les vérifications de signature dans un mode spécifique.
En l'absence de l'un de ces éléments, la vérification de signature empêchera l'exploitation malgré la présence d'un deserialization sink.
Pre-auth reachability via error-handling flows
Même lorsqu'un endpoint de deserialization semble exiger une authentification ou un token lié à la session, le code de gestion des erreurs peut involontairement générer et attacher le token à une session non authentifiée.
Example reachability chain (GoAnywhere MFT):
- Target servlet: /goanywhere/lic/accept/
requires a session-bound license request token. - Error path: hitting /goanywhere/license/Unlicensed.xhtml with trailing junk and invalid JSF state triggers AdminErrorHandlerServlet, which does:
- SessionUtilities.generateLicenseRequestToken(session)
- Redirects to vendor license server with a signed license request in bundle=<...>
- The bundle can be decrypted offline (hard-coded keys) to recover the GUID. Keep the same session cookie and POST to /goanywhere/lic/accept/
with attacker-controlled bundle bytes, reaching the 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 corrigé : 302 Location header to https://my.goanywhere.com/lic/request?bundle=... and Set-Cookie: ASESSIONID=...
- Corrigé : redirection sans bundle (pas de génération de token).
Détection Blue-team
Les indicateurs dans les traces de pile/logs suggèrent fortement des tentatives visant un 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
Conseils de durcissement
- Maintenez la vérification de signature avant tout appel à getObject() et assurez-vous que la vérification utilise la clé publique/algorithme prévu.
- Remplacez les appels directs à SignedObject.getObject() par un wrapper renforcé qui réapplique le filtrage au flux interne (par ex., deserializeUntrustedSignedObject utilisant des listes d'autorisation ValidatingObjectInputStream/ObjectInputFilter).
- Supprimez les flux de gestion des erreurs qui émettent des jetons liés à la session pour des utilisateurs non authentifiés. Traitez les chemins d'erreur comme une surface d'attaque.
- Privilégiez les filtres de sérialisation Java (JEP 290) avec des listes d'autorisation strictes pour les désérialisations externe et interne. Exemple :
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
Récapitulatif de la chaîne d'attaque (CVE-2025-10035)
- Pre-auth token minting via le gestionnaire d'erreurs:
GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate
Recevoir un 302 avec bundle=... et ASESSIONID=... ; décrypter le bundle hors ligne pour récupérer le GUID.
- Accéder au sink pre-auth avec le même cookie :
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded
bundle=<attacker-controlled-bytes>
- RCE requires a correctly signed SignedObject wrapping a gadget chain. Les chercheurs n'ont pas pu contourner la vérification des signatures ; l'exploitation dépend de l'accès à la clé privée correspondante ou à un oracle de signature.
Fixed versions and behavioural changes
- GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:
- Renforcer la désérialisation interne en remplaçant SignedObject.getObject() par un wrapper (deserializeUntrustedSignedObject).
- Supprimer la génération du token du gestionnaire d'erreurs, fermant l'accès pré-auth.
Notes on JSF/ViewState
Le reachability trick exploite une page JSF (.xhtml) et un javax.faces.ViewState invalide pour rediriger vers un gestionnaire d'erreurs privilégié. Bien que ce ne soit pas un problème de désérialisation JSF, c'est un pattern pré-auth récurrent : s'introduire dans des gestionnaires d'erreurs qui exécutent des actions privilégiées et définissent des attributs de session pertinents pour la sécurité.
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
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.