Java SignedObject 门控的反序列化和通过错误路径的预认证可到达性

Reading time: 9 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 反序列化模式,该模式以 java.security.SignedObject 为核心,以及看似不可到达的 sink 如何通过错误处理流程变为预认证可到达。该技术在 Fortra GoAnywhere MFT (CVE-2025-10035) 中被观察到,但也适用于类似设计。

威胁模型

  • 攻击者可以访问一个 HTTP 端点,该端点最终处理攻击者提供的 byte[],该 byte[] 旨在作为序列化的 SignedObject。
  • 代码使用一个验证包装器(例如 Apache Commons IO ValidatingObjectInputStream 或自定义适配器)来将最外层类型限制为 SignedObject(或 byte[])。
  • SignedObject.getObject() 返回的内部对象是 gadget 链可能触发的地方(例如 CommonsBeanutils1),但只有在签名验证门通过之后。

典型的易受攻击模式

下面是基于 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();
}
}

关键观察:

  • 位于 (1) 的验证反序列化器会阻止任意顶层 gadget classes;仅接受 SignedObject(或 raw byte[])。
  • RCE 原语位于由 SignedObject.getObject() 在 (3) 处实例化的内部对象中。
  • 位于 (2) 的签名门控强制要求 SignedObject 必须针对产品内置的公钥通过 verify()。除非攻击者能够生成有效签名,否则内部 gadget 永远不会被反序列化。

利用注意事项

要实现代码执行,攻击者必须提供一个正确签名的 SignedObject,内部对象封装一个恶意 gadget chain。通常需要以下之一:

  • Private key compromise: 获取产品用于签名/验证 license 对象的匹配私钥。
  • Signing oracle: 强迫厂商或受信任的签名服务对攻击者控制的序列化内容进行签名(例如,如果 license server 对来自客户端输入的嵌入任意对象进行签名)。
  • Alternate reachable path: 找到一个在服务器端反序列化内部对象但不强制执行 verify() 的路径,或在特定模式下跳过签名检查的路径。

如果没有上述任一情况,即使存在 deserialization sink,签名验证也会阻止利用。

通过错误处理流程的预认证可达性

即便某个反序列化端点看似需要认证或会话绑定的令牌,错误处理代码也可能无意中为未认证会话生成并附加该令牌。

示例可达链(GoAnywhere MFT):

  • Target servlet: /goanywhere/lic/accept/ 需要会话绑定的 license request token。
  • Error path: 访问 /goanywhere/license/Unlicensed.xhtml(附带尾随垃圾和无效的 JSF 状态)会触发 AdminErrorHandlerServlet,其执行:
    • SessionUtilities.generateLicenseRequestToken(session)
    • 重定向到厂商 license server,带有在 bundle=<...> 中的已签名 license 请求
  • 该 bundle 可以离线解密(硬编码密钥)以恢复 GUID。保留相同的会话 cookie 并向 /goanywhere/lic/accept/ POST 攻击者控制的 bundle 字节,即可在未认证状态下触达 SignedObject sink。

Proof-of-reachability (impact-less) probe:

http
GET /goanywhere/license/Unlicensed.xhtml/x?javax.faces.ViewState=x&GARequestAction=activate HTTP/1.1
Host: <target>
  • 未修补: 302 Location header 指向 https://my.goanywhere.com/lic/request?bundle=... 并且 Set-Cookie: ASESSIONID=...
  • 已修补: 重定向不带 bundle(不生成 token)。

蓝队检测

堆栈跟踪/日志中的迹象强烈表明尝试命中 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() 调用替换为一个经过强化的包装器,该包装器会对内部流重新应用过滤(例如,使用 ValidatingObjectInputStream/ObjectInputFilter 的 deserializeUntrustedSignedObject 和 allow-lists)。
  • 移除在错误处理流程中为未认证用户签发会话绑定令牌的做法。将错误路径视为攻击面。
  • 优先使用 Java serialization filters (JEP 290),对外层和内层的反序列化都使用严格的 allow-lists。示例:
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 requires a correctly signed SignedObject wrapping a gadget chain. 研究人员无法绕过签名验证;利用取决于获得匹配的 private key 或 signing oracle。

Fixed versions and behavioural changes

  • GoAnywhere MFT 7.8.4 and Sustain Release 7.6.3:
  • 通过将 SignedObject.getObject() 替换为一个包装器 (deserializeUntrustedSignedObject) 来强化内部反序列化。
  • 移除 error-handler 的 token 生成,关闭 pre-auth 的可到达性。

Notes on JSF/ViewState

该可到达性技巧利用了一个 JSF 页面 (.xhtml) 和无效的 javax.faces.ViewState,将流程导入到一个有特权的错误处理器。虽然这不是一个 JSF 反序列化问题,但它是一个反复出现的 pre-auth 模式:进入执行有特权操作并设置安全相关会话属性的错误处理器。

References

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