java-signedobject-gated-deserialization.md (8564B)
1 --- 2 title: "Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/deserialization/java-signedobject-gated-deserialization.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/deserialization/java-signedobject-gated-deserialization.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths 14 15 This page documents a common "guarded" Java deserialization pattern built around java.security.SignedObject and how seemingly unreachable sinks can become pre-auth reachable via error-handling flows. The technique was observed in Fortra GoAnywhere MFT (CVE-2025-10035) but is applicable to similar designs.<sup>[[1]](#references)</sup> 16 17 ## Threat model 18 19 - Attacker can reach an HTTP endpoint that eventually processes an attacker-supplied byte[] intended to be a serialized SignedObject. 20 - The code uses a validating wrapper (e.g., Apache Commons IO ValidatingObjectInputStream or a custom adapter) to constrain the outermost type to SignedObject (or byte[]). 21 - The inner object returned by SignedObject.getObject() is where gadget chains can trigger (e.g., CommonsBeanutils1), but only after a signature verification gate. 22 23 ## Typical vulnerable pattern 24 25 A simplified example based on com.linoma.license.gen2.BundleWorker.verify: 26 27 ```java 28 private static byte[] verify(byte[] payload, KeyConfig keyCfg) throws Exception { 29 String sigAlg = "SHA1withDSA"; 30 if ("2".equals(keyCfg.getVersion())) { 31 sigAlg = "SHA512withRSA"; // key version controls algorithm 32 } 33 PublicKey pub = getPublicKey(keyCfg); 34 Signature sig = Signature.getInstance(sigAlg); 35 36 // 1) Outer, "guarded" deserialization restricted to SignedObject 37 SignedObject so = (SignedObject) JavaSerializationUtilities.deserialize( 38 payload, SignedObject.class, new Class[]{ byte[].class }); 39 40 if (keyCfg.isServer()) { 41 // Hardened server path 42 return ((SignedContainer) JavaSerializationUtilities.deserializeUntrustedSignedObject( 43 so, SignedContainer.class, new Class[]{ byte[].class } 44 )).getData(); 45 } else { 46 // 2) Signature check using a baked-in public key 47 if (!so.verify(pub, sig)) { 48 throw new IOException("Unable to verify signature!"); 49 } 50 // 3) Inner object deserialization (potential gadget execution) 51 SignedContainer inner = (SignedContainer) so.getObject(); 52 return inner.getData(); 53 } 54 } 55 ``` 56 57 Key observations: 58 - The validating deserializer at (1) blocks arbitrary top-level gadget classes; only SignedObject (or raw byte[]) is accepted. 59 - The RCE primitive would be in the inner object materialized by SignedObject.getObject() at (3). 60 - A signature gate at (2) enforces that the SignedObject must verify against a product-baked public key. Unless the attacker can produce a valid signature, the inner gadget never deserializes. 61 62 ## Exploitation considerations 63 64 To achieve code execution, an attacker must deliver a correctly signed SignedObject that wraps a malicious gadget chain as its inner object. This generally requires one of the following: 65 66 - Private key compromise: obtain the matching private key used by the product to sign/verify license objects. 67 - Signing oracle: coerce the vendor or a trusted signing service to sign attacker-controlled serialized content (e.g., if a license server signs an embedded arbitrary object from client input). 68 - Alternate reachable path: find a server-side path that deserializes the inner object without enforcing verify(), or that skips signature checks under a specific mode. 69 70 Absent one of these, signature verification will prevent exploitation despite the presence of a deserialization sink. 71 72 ## Pre-auth reachability via error-handling flows 73 74 Even when a deserialization endpoint appears to require authentication or a session-bound token, error-handling code can inadvertently mint and attach the token to an unauthenticated session. 75 76 Example reachability chain (GoAnywhere MFT): 77 - Target servlet: /goanywhere/lic/accept/<GUID> requires a session-bound license request token. 78 - Error path: hitting /goanywhere/license/Unlicensed.xhtml with trailing junk and invalid JSF state triggers AdminErrorHandlerServlet, which does: 79 - SessionUtilities.generateLicenseRequestToken(session) 80 - Redirects to vendor license server with a signed license request in bundle=<...> 81 - The bundle can be decrypted offline (hard-coded keys) to recover the GUID. Keep the same session cookie and POST to /goanywhere/lic/accept/<GUID> with attacker-controlled bundle bytes, reaching the SignedObject sink pre-auth.<sup>[[1]](#references)</sup> 82 83 Proof-of-reachability (impact-less) probe: 84 85 ```http 86 GET /goanywhere/license/Unlicensed.xhtml/x?javax.faces.ViewState=x&GARequestAction=activate HTTP/1.1 87 Host: <target> 88 ``` 89 90 - Unpatched: 302 Location header to https://my.goanywhere.com/lic/request?bundle=... and Set-Cookie: ASESSIONID=... 91 - Patched: redirect without bundle (no token generation). 92 93 ## Blue-team detection 94 95 Indicators in stack traces/logs strongly suggest attempts to hit a SignedObject-gated sink:<sup>[[1]](#references)</sup> 96 97 ```text 98 java.io.ObjectInputStream.readObject 99 java.security.SignedObject.getObject 100 com.linoma.license.gen2.BundleWorker.verify 101 com.linoma.license.gen2.BundleWorker.unbundle 102 com.linoma.license.gen2.LicenseController.getResponse 103 com.linoma.license.gen2.LicenseAPI.getResponse 104 com.linoma.ga.ui.admin.servlet.LicenseResponseServlet.doPost 105 ``` 106 107 ## Hardening guidance 108 109 - Maintain signature verification before any getObject() call and ensure the verification uses the intended public key/algorithm. 110 - Replace direct SignedObject.getObject() calls with a hardened wrapper that re-applies filtering to the inner stream (e.g., deserializeUntrustedSignedObject using ValidatingObjectInputStream/ObjectInputFilter allow-lists). 111 - Remove error-handler flows that issue session-bound tokens for unauthenticated users. Treat error paths as attack surface. 112 - Prefer Java serialization filters (JEP 290) with strict allow-lists for both outer and inner deserializations. Example: 113 114 ```java 115 ObjectInputFilter filter = info -> { 116 Class<?> c = info.serialClass(); 117 if (c == null) return ObjectInputFilter.Status.UNDECIDED; 118 if (c == java.security.SignedObject.class || c == byte[].class) return ObjectInputFilter.Status.ALLOWED; 119 return ObjectInputFilter.Status.REJECTED; // outer layer 120 }; 121 ObjectInputFilter.Config.setSerialFilter(filter); 122 // For the inner object, apply a separate strict DTO allow-list 123 ``` 124 125 ## Example attack chain recap (CVE-2025-10035) 126 127 1) Pre-auth token minting via error handler: 128 129 ```http 130 GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate 131 ``` 132 133 Receive 302 with bundle=... and ASESSIONID=...; decrypt bundle offline to recover GUID. 134 135 2) Reach the sink pre-auth with same cookie: 136 137 ```http 138 POST /goanywhere/lic/accept/<GUID> HTTP/1.1 139 Cookie: ASESSIONID=<value> 140 Content-Type: application/x-www-form-urlencoded 141 142 bundle=<attacker-controlled-bytes> 143 ``` 144 145 3) RCE requires a correctly signed SignedObject wrapping a gadget chain. Researchers could not bypass signature verification; exploitation hinges on access to a matching private key or a signing oracle.<sup>[[1]](#references)</sup> 146 147 ## Fixed versions and behavioural changes 148 149 - GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:<sup>[[2]](#references)</sup> 150 - Harden inner deserialization by replacing SignedObject.getObject() with a wrapper (deserializeUntrustedSignedObject). 151 - Remove error-handler token generation, closing pre-auth reachability. 152 153 ## Notes on JSF/ViewState 154 155 The reachability trick leverages a JSF page (.xhtml) and invalid javax.faces.ViewState to route into a privileged error handler. While not a JSF deserialization issue, it’s a recurring pre-auth pattern: break into error handlers that perform privileged actions and set security-relevant session attributes. 156 157 ## References 158 159 - [1] [watchTowr Labs – Is This Bad? This Feels Bad — GoAnywhere CVE-2025-10035](https://labs.watchtowr.com/is-this-bad-this-feels-bad-goanywhere-cve-2025-10035/) 160 - [2] [Fortra advisory FI-2025-012 – Deserialization Vulnerability in GoAnywhere MFT's License Servlet](https://www.fortra.com/security/advisories/product-security/fi-2025-012)