Java SignedObjectゲート付きデシリアライズとエラー経路による認証前到達可能性

Reading time: 10 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

このページでは、java.security.SignedObject を中心に構築された一般的な「ガードされた」Javaデシリアライズのパターンと、見た目上到達不能なシンクがエラー処理フローを介して認証前に到達可能になる仕組みを説明します。 この手法は Fortra GoAnywhere MFT (CVE-2025-10035) で観測されましたが、類似の設計にも適用可能です。

脅威モデル

  • 攻撃者は最終的にシリアライズされた SignedObject を意図した攻撃者提供の byte[] を処理する HTTP エンドポイントに到達できる。
  • コードは検証ラッパー(例: Apache Commons IO ValidatingObjectInputStream やカスタムアダプタ)を使用して、最外層の型を SignedObject(または byte[])に制限している。
  • SignedObject.getObject() により返される内部オブジェクトが、例えば CommonsBeanutils1 のような gadget chains を誘発する場所になり得るが、署名検証のゲートの後でのみ発動する。

典型的な脆弱パターン

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();
}
}

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.

エクスプロイトに関する考慮点

コード実行を達成するために、攻撃者は内部オブジェクトとして malicious gadget chain をラップした、正しく署名された SignedObject を届ける必要がある。これは一般に次のいずれかを必要とする:

  • Private key compromise: 製品が license オブジェクトの署名/検証に使用する対応する private key を取得する。
  • Signing oracle: ベンダーや信頼された signing service に、攻撃者制御のシリアライズ済みコンテンツに署名させる(例: license server がクライアント入力から埋め込まれた任意のオブジェクトに署名する場合)。
  • Alternate reachable path: verify() を強制しない、あるいは特定モードで署名チェックをスキップするサーバー側パスを見つける。

これらのいずれかがなければ、署名検証によりデシリアライズのシンクが存在してもエクスプロイトは阻止される。

エラー処理フローを介した認証前の到達可能性

デシリアライズエンドポイントが認証やセッションに紐付いたトークンを要求しているように見えても、エラー処理コードが誤ってそのトークンを未認証セッションに発行・付与してしまうことがある。

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.

到達性の証明(影響なし)プローブ:

http
GET /goanywhere/license/Unlicensed.xhtml/x?javax.faces.ViewState=x&GARequestAction=activate HTTP/1.1
Host: <target>
  • 未修正: 302 Location header to https://my.goanywhere.com/lic/request?bundle=... and Set-Cookie: ASESSIONID=...
  • 修正済み: redirect without bundle (no token generation).

ブルーチームの検出

stack traces/logs におけるインジケータは、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

ハードニングのガイダンス

  • すべての getObject() 呼び出しの前に署名検証を行い、検証が意図した公開鍵/アルゴリズムを使用していることを確認する。
  • 直接の SignedObject.getObject() 呼び出しを、内部ストリームへフィルタを再適用するハード化されたラッパーに置き換える(例: deserializeUntrustedSignedObject を使い、ValidatingObjectInputStream/ObjectInputFilter の許可リストを適用する)。
  • 認証されていないユーザーに対してセッションに紐づくトークンを発行するエラーハンドラのフローを削除する。エラー経路は攻撃対象面として扱う。
  • 外側および内側のデシリアライズの両方に対して厳格な許可リストを持つ Java serialization filters (JEP 290) の使用を推奨する。例:
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

攻撃チェーンの例 (CVE-2025-10035)

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

bundle=... と ASESSIONID=... を含む 302 を受け取り、bundle をオフラインで復号して GUID を復元する。

  1. 同じ cookie で pre-auth の sink に到達する:
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 は、gadget chain をラップした正しく署名された SignedObject を必要とします。研究者は署名検証を回避できませんでした。悪用は一致する秘密鍵または signing oracle へのアクセスに依存します。

修正バージョンと挙動の変更

  • GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:
  • 内部の deserialization を強化するために SignedObject.getObject() を wrapper (deserializeUntrustedSignedObject) に置き換えました。
  • error-handler の token 生成を削除し、pre-auth reachability を閉じました。

JSF/ViewState に関する注意事項

この reachability trick は、JSF ページ (.xhtml) と無効な javax.faces.ViewState を利用して、特権的な error handler にルーティングします。JSF の deserialization の問題ではありませんが、繰り返し見られる pre-auth パターンであり、特権的な操作を実行し、セキュリティ関連の session attributes を設定する error handler に侵入するという手口です。

参考

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする