daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)