Java SignedObject-gated Deserialization und Pre-auth Reachability über Error Paths

Reading time: 7 minutes

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Diese Seite dokumentiert ein häufiges "guarded" Java Deserialization-Muster, das um java.security.SignedObject aufgebaut ist, und wie scheinbar unerreichbare sinks durch Fehlerbehandlungsabläufe pre-auth erreichbar werden können. Die Technik wurde in Fortra GoAnywhere MFT (CVE-2025-10035) beobachtet, ist aber auf ähnliche Designs anwendbar.

Bedrohungsmodell

  • Ein Angreifer kann einen HTTP-Endpunkt erreichen, der schließlich ein vom Angreifer bereitgestelltes byte[] verarbeitet, das als serialisiertes SignedObject gedacht ist.
  • Der Code verwendet einen validierenden Wrapper (z. B. Apache Commons IO ValidatingObjectInputStream oder einen eigenen Adapter), um den äußersten Typ auf SignedObject (oder byte[]) zu beschränken.
  • Das innere Objekt, das von SignedObject.getObject() zurückgegeben wird, ist der Ort, an dem Gadget-Ketten (z. B. CommonsBeanutils1) ausgelöst werden können, jedoch nur nach einem Signatur-Verifizierungs-Gate.

Typisches verwundbares Muster

Ein vereinfachtes Beispiel basierend auf 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();
}
}

Wichtige Beobachtungen:

  • Der validierende Deserializer bei (1) blockiert beliebige Top-Level-Gadget-Klassen; nur SignedObject (oder raw byte[]) wird akzeptiert.
  • Das RCE-Primitive läge im inneren Objekt, das durch SignedObject.getObject() bei (3) materialisiert wird.
  • Eine Signaturprüfung bei (2) erzwingt, dass das SignedObject gegen einen im Produkt eingebetteten öffentlichen Schlüssel verifiziert werden muss. Solange der Angreifer keine gültige Signatur erzeugen kann, wird das innere Gadget niemals deserialisiert.

Überlegungen zur Ausnutzung

Um Codeausführung zu erreichen, muss ein Angreifer ein korrekt signiertes SignedObject liefern, das eine bösartige Gadget-Kette als inneres Objekt umschließt. Dies erfordert in der Regel eines der folgenden:

  • Private key compromise: den passenden privaten Schlüssel erlangen, der vom Produkt zum Signieren/Verifizieren von Lizenzobjekten verwendet wird.
  • Signing oracle: den Vendor oder einen vertrauenswürdigen Signing-Service dazu bringen, vom Angreifer kontrollierte serialisierte Inhalte zu signieren (z. B. wenn ein License-Server ein eingebettetes beliebiges Objekt aus Client-Eingaben signiert).
  • Alternate reachable path: einen serverseitigen Pfad finden, der das innere Objekt deserialisiert, ohne verify() durchzusetzen, oder die Signaturprüfung unter einem bestimmten Modus überspringt.

Fehlt einer dieser Fälle, verhindert die Signaturprüfung eine Ausnutzung trotz der Existenz einer Deserialisierungs-Senke.

Pre-auth-Erreichbarkeit über Fehlerbehandlungsflüsse

Selbst wenn ein Deserialisierungs-Endpunkt scheinbar Authentifizierung oder ein sessionsgebundenes Token erfordert, kann Fehlerbehandlungscode versehentlich das Token erstellen und an eine nicht authentifizierte Session anhängen.

Beispielhafte Erreichbarkeitskette (GoAnywhere MFT):

  • Target servlet: /goanywhere/lic/accept/ erfordert ein sessionsgebundenes license request token.
  • Error path: Aufruf von /goanywhere/license/Unlicensed.xhtml mit angehängtem Junk und ungültigem JSF-State löst AdminErrorHandlerServlet aus, das folgendes macht:
    • SessionUtilities.generateLicenseRequestToken(session)
    • Leitet an den Vendor-License-Server weiter mit einer signierten license request in bundle=<...>
    • Das bundle kann offline (hard-coded keys) entschlüsselt werden, um die GUID wiederherzustellen. Behalte das gleiche Session-Cookie und sende ein POST an /goanywhere/lic/accept/ mit attacker-controlled bundle bytes, wodurch der SignedObject sink pre-auth erreicht wird.

Erreichbarkeitsnachweis (ohne Auswirkungen) Probe:

http
GET /goanywhere/license/Unlicensed.xhtml/x?javax.faces.ViewState=x&GARequestAction=activate HTTP/1.1
Host: <target>
  • Ungepatcht: 302 Location header to https://my.goanywhere.com/lic/request?bundle=... and Set-Cookie: ASESSIONID=...
  • Gepatcht: redirect without bundle (keine Token-Generierung).

Blue-Team-Erkennung

Indikatoren in Stack-Traces/Logs deuten stark auf Versuche hin, einen SignedObject-gated sink anzusprechen:

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

Härtungsleitfaden

  • Führen Sie die Signaturprüfung vor jedem getObject()-Aufruf durch und stellen Sie sicher, dass die Prüfung den vorgesehenen Public Key/Algorithmus verwendet.
  • Ersetzen Sie direkte SignedObject.getObject()-Aufrufe durch einen gehärteten Wrapper, der Filter erneut auf den inneren Stream anwendet (z. B. deserializeUntrustedSignedObject unter Verwendung von ValidatingObjectInputStream/ObjectInputFilter allow-lists).
  • Entfernen Sie Error-Handler-Flows, die session-gebundene Tokens für nicht authentifizierte Benutzer ausgeben. Behandeln Sie Fehlerpfade als Angriffsfläche.
  • Bevorzugen Sie Java serialization filters (JEP 290) mit strengen allow-lists sowohl für die äußere als auch die innere Deserialisierung. Beispiel:
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

Zusammenfassung der Beispiel-Angriffskette (CVE-2025-10035)

  1. Pre-auth token minting via error handler:
http
GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate

Erhalte 302 mit bundle=... und ASESSIONID=...; decrypt bundle offline, um GUID wiederherzustellen.

  1. Erreiche den sink pre-auth mit demselben 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 erfordert ein korrekt signiertes SignedObject, das eine gadget chain umschließt. Forscher konnten die Signaturprüfung nicht umgehen; die Ausnutzung hängt vom Zugriff auf einen passenden privaten Schlüssel oder ein signing oracle ab.

Behebte Versionen und Verhaltensänderungen

  • GoAnywhere MFT 7.8.4 und Sustain Release 7.6.3:
  • Härtet die interne Deserialisierung, indem SignedObject.getObject() durch einen Wrapper (deserializeUntrustedSignedObject) ersetzt wird.
  • Entfernt die Token-Generierung des Error-Handlers und schließt damit die pre-auth Erreichbarkeit.

Hinweise zu JSF/ViewState

Der Reachability-Trick nutzt eine JSF-Seite (.xhtml) und einen invaliden javax.faces.ViewState, um in einen privilegierten Error-Handler zu routen. Obwohl es kein JSF-Deserialisierungsproblem ist, handelt es sich um ein wiederkehrendes pre-auth-Muster: sich in Error-Handler hineinzubrechen, die privilegierte Aktionen ausführen und sicherheitsrelevante Session-Attribute setzen.

References

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks