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
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
本页记录了一个常见的“受保护”的 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 的简化示例:
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:
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。示例:
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)
- Pre-auth token minting via error handler:
GET /goanywhere/license/Unlicensed.xhtml/watchTowr?javax.faces.ViewState=watchTowr&GARequestAction=activate
收到带有 bundle=... 和 ASESSIONID=... 的 302;离线解密 bundle 来恢复 GUID。
- 使用相同的 cookie 在 pre-auth 阶段到达 sink:
POST /goanywhere/lic/accept/<GUID> HTTP/1.1
Cookie: ASESSIONID=<value>
Content-Type: application/x-www-form-urlencoded
bundle=<attacker-controlled-bytes>
- 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
- watchTowr Labs – Is This Bad? This Feels Bad — GoAnywhere CVE-2025-10035
- Fortra advisory FI-2025-012 – Deserialization Vulnerability in GoAnywhere MFT's License Servlet
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
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。