hacktricks/src/pentesting-web/deserialization/java-signedobject-gated-deserialization.md

8.2 KiB
Raw Blame History

Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths

{{#include ../../banners/hacktricks-training.md}}

Esta página documenta um padrão comum de desserialização Java "guardado" construído em torno de java.security.SignedObject e como sinks aparentemente inacessíveis podem tornar-se alcançáveis pré-auth via fluxos de tratamento de erro. A técnica foi observada no Fortra GoAnywhere MFT (CVE-2025-10035), mas é aplicável a designs similares.

Threat model

  • Um atacante pode alcançar um endpoint HTTP que eventualmente processa um byte[] fornecido pelo atacante, destinado a ser um SignedObject serializado.
  • O código usa um wrapper de validação (por exemplo, Apache Commons IO ValidatingObjectInputStream ou um adaptador customizado) para restringir o tipo mais externo a SignedObject (ou byte[]).
  • O objeto interno retornado por SignedObject.getObject() é onde cadeias de gadgets podem ser acionadas (por exemplo, CommonsBeanutils1), mas somente após um gate de verificação de assinatura.

Typical vulnerable pattern

Um exemplo simplificado baseado em 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();
}
}

Key observations:

  • The validating deserializer at (1) blocks arbitrary top-level gadget classes; only SignedObject (or raw byte[]) is accepted.
  • The RCE primitive would be in the inner object materialized by SignedObject.getObject() at (3).
  • 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.

Considerações de exploração

Para atingir execução de código, um atacante deve entregar um SignedObject corretamente assinado que envolva um malicious gadget chain como seu objeto interno. Isso geralmente requer uma das seguintes opções:

  • Comprometimento da chave privada: obter a chave privada correspondente usada pelo produto para assinar/verificar objetos de licença.
  • Signing oracle: coagir o fornecedor ou um serviço de signing confiável a assinar conteúdo serializado controlado pelo atacante (por exemplo, se um license server assina um objeto arbitrário embutido a partir da entrada do cliente).
  • Caminho alternativo alcançável: encontrar um caminho no lado servidor que desserialize o objeto interno sem aplicar verify(), ou que pule verificações de assinatura sob um modo específico.

Na ausência de uma dessas, a verificação de assinatura impedirá a exploração apesar da presença de um deserialization sink.

Acessibilidade pré-auth via fluxos de tratamento de erro

Mesmo quando um endpoint de deserialização parece exigir autenticação ou um token vinculado à sessão, o código de tratamento de erro pode inadvertidamente gerar e anexar o token a uma sessão não autenticada.

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>
  • Não corrigido: 302 Location header to https://my.goanywhere.com/lic/request?bundle=... and Set-Cookie: ASESSIONID=...
  • Corrigido: redirecionamento sem bundle (sem geração de token).

Blue-team detection

Indicadores em stack traces/logs sugerem fortemente tentativas de atingir um 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

Diretrizes de hardening

  • Mantenha a verificação de assinatura antes de qualquer chamada getObject() e assegure que a verificação use a chave pública/algoritmo pretendidos.
  • Substitua chamadas diretas a SignedObject.getObject() por um wrapper endurecido que reaplica filtragem ao fluxo interno (por exemplo, deserializeUntrustedSignedObject usando ValidatingObjectInputStream/ObjectInputFilter allow-lists).
  • Remova fluxos de tratamento de erro que emitam session-bound tokens para usuários não autenticados. Trate caminhos de erro como superfície de ataque.
  • Prefira Java serialization filters (JEP 290) com allow-lists estritas tanto para a desserialização externa quanto para a interna. Exemplo:
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

Recapitulação da cadeia de ataque de exemplo (CVE-2025-10035)

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

Receba 302 com bundle=... e ASESSIONID=...; descriptografe o bundle offline para recuperar o GUID.

  1. Alcance o sink pre-auth com o mesmo cookie:
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded

bundle=<attacker-controlled-bytes>
  1. RCE requer um SignedObject corretamente assinado encapsulando uma gadget chain. Pesquisadores não conseguiram contornar a verificação de assinatura; a exploração depende do acesso a uma private key correspondente ou a um signing oracle.

Versões corrigidas e mudanças de comportamento

  • GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:
  • Harden inner deserialization by replacing SignedObject.getObject() with a wrapper (deserializeUntrustedSignedObject).
  • Remover a geração de token do error-handler, encerrando a pre-auth reachability.

Observações sobre JSF/ViewState

A técnica de reachability explora uma página JSF (.xhtml) e um javax.faces.ViewState inválido para direcionar para um error handler privilegiado. Embora não seja um problema de deserialização do JSF, é um padrão recorrente de pre-auth: invadir error handlers que executam ações privilegiadas e definem atributos de sessão relevantes para a segurança.

References

{{#include ../../banners/hacktricks-training.md}}