Feb 25, 2026·5 min read·9 visits
Critical Remote Code Execution (RCE) in Apache Struts 2 via the Jakarta Multipart parser. Attackers can execute arbitrary system commands by sending a malicious OGNL expression in the 'Content-Type' HTTP header. No authentication is required.
In the annals of cybersecurity history, few vulnerabilities have caused as much chaos, financial ruin, and executive sweating as CVE-2017-5638. It is the perfect storm: a widely used enterprise framework (Apache Struts 2), a trivial exploitation vector (an HTTP header), and a root cause rooted in over-helpful error handling. This vulnerability famously facilitated the 2017 Equifax data breach, exposing the personal information of 147 million people. It serves as a stark reminder that even the most robust fortresses can be toppled if the doorman decides to execute instructions scribbled on a suspicious package.
Imagine you're a bouncer at a club. Someone tries to hand you a fake ID. Instead of just saying "Access Denied," you read the name on the ID out loud over the club's PA system. Now imagine the name on the ID isn't a name, but a magic spell that causes the club to explode.
That is essentially CVE-2017-5638. Apache Struts 2, a heavy-lifting framework for Java web applications, uses the Jakarta Multipart parser to handle file uploads. When a user tries to upload a file, they send a multipart/form-data request. If the request is malformed—say, the Content-Type header is garbage—the parser throws an exception.
The framework, trying to be helpful, catches this exception and attempts to generate a localized error message to send back to the user. The problem? It includes the raw content of the invalid header in the error message processing pipeline. And in Struts 2, that pipeline processes OGNL (Object-Graph Navigation Language).
This means if an attacker puts an OGNL payload in the Content-Type header, Struts doesn't just reject it; it executes it while trying to tell you it's invalid.
Let's dig into the JakartaMultiPartRequest.java class. When an upload error occurs (like an invalid Content-Type), the code calls buildErrorMessage. This method eventually calls LocalizedTextUtil.findText.
Here is the logic flow of the failure:
The fatal flaw lies in how findText was called. The developers passed the exception message (which contains the attacker's string) as the default message key. The Struts localization system is designed to parse OGNL expressions in message keys to allow for dynamic text generation. By passing untrusted input into a function designed to evaluate expressions, Struts effectively handed the keys to the kingdom to anyone with curl.
The fix was painfully simple, which makes the exploit even more tragic. The vulnerability existed because the exception message e.getMessage() was passed as the 4th argument to findText.
Vulnerable Code (Before):
// The 'e.getMessage()' is passed as the default message key
// If the key isn't found, Struts evaluates this string for OGNL.
return LocalizedTextUtil.findText(
this.getClass(),
errorKey,
defaultLocale,
e.getMessage(), // <--- THE KILL ZONE
args
);Patched Code (After):
// First, check if the errorKey exists.
if (LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, new Object[0]) == null) {
// If not, use a safe default key and pass the exception message as an ARGUMENT
return LocalizedTextUtil.findText(
this.getClass(),
"struts.messages.error.uploading",
defaultLocale,
null,
new Object[] { e.getMessage() } // <--- Safe as an argument
);
} else {
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, args);
}In the patched version, the dangerous string e.getMessage() is wrapped in an Object[] array and passed as an argument. Arguments are treated as literal strings and are not recursively evaluated for OGNL expressions. It's the difference between eval(input) and print(input).
Struts 2 has security mechanisms to prevent OGNL from doing dangerous things, like accessing static methods or modifying system properties. However, OGNL is incredibly flexible. The exploit payload for CVE-2017-5638 is a masterclass in sandbox evasion.
The payload typically looks like this (formatted for readability):
%{
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#process=@java.lang.Runtime@getRuntime().exec('cat /etc/passwd')).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).
(#ros.flush())
}Wait, it's not that simple. By Struts 2.3.x, strict OGNL security was in place. Attackers had to explicitly overwrite the _memberAccess object to regain access to private members and static methods.
The "Universal" Payload:
Attackers set Content-Type to a massive string starting with:
%{(#_='multipart/form-data')...
They use reflection to reset #_memberAccess to DEFAULT_MEMBER_ACCESS (which allows everything). Once they have that, they can instantiate java.lang.ProcessBuilder. Because this happens before the request is fully processed or logged by the application logic, standard application logs often show nothing until it's too late.
This is a System-Level RCE. It doesn't get worse than this. The application server (likely Tomcat or JBoss) is usually running with significant privileges.
The fact that this can be triggered by a single curl command against the login page (or any page using the multipart parser) makes it a "spray and pray" favorite for script kiddies and nation-states alike.
If you are running Struts 2.3.x < 2.3.32 or 2.5.x < 2.5.10.1, you are vulnerable. Stop reading and patch.
Remediation Steps:
Content-Type header contains %, #, or multipart/form-data followed by OGNL syntax.> [!WARNING]
> Blocking multipart/form-data entirely will break file uploads. You must specifically look for OGNL markers like %{ or ${ inside the header value.
cos or pell multipart parsers instead of jakarta in your struts.xml, though this may have functional side effects.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Apache Struts 2 Apache | 2.3.5 - 2.3.31 | 2.3.32 |
Apache Struts 2 Apache | 2.5.0 - 2.5.10 | 2.5.10.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-917 |
| Attack Vector | Network (HTTP Header) |
| CVSS v3.1 | 9.8 (Critical) |
| Exploit Status | Active / Weaponized |
| KEV Listed | Yes |
| EPSS Score | 0.9427 (Very High) |
Improper Neutralization of Special Elements used in an Expression Language Statement ('Expression Language Injection')