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 ์ง€์›ํ•˜๊ธฐ

์ด ํŽ˜์ด์ง€๋Š” 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)

  1. 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๋ฅผ ๋ณต๊ตฌ.

  1. ๋™์ผํ•œ 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>
  1. 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

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 ์ง€์›ํ•˜๊ธฐ