flawopen.com/Incidents/equifax-struts-ognl-rce-july-2017
Equifax breach: CVE-2017-5638 and nearly 150 million people
CVE-2017-5638 let attackers inject OGNL through the Content-Type header in Equifax’s unpatched Struts portal. The attack lasted about 76 days and exposed data affecting nearly 150 million people.[1], [4]
Imagine a mailroom software that turns a rejected shipping label into an error notice. If the notice treats the label as a live instruction template, a sender can make the software follow instructions instead of merely reporting a bad label. Struts had a fix before the breach, but Equifax’s notice went to an outdated administrator list and a network scan failed to detect the vulnerability on the portal.[1], [4]
Core Concepts & Subsystem Terms
OGNL- Object-Graph Navigation Language, the expression language Struts uses to read and set Java object properties. Evaluating attacker text as OGNL means running the attacker's code.[1], [6]
Jakarta multipart parser- The Struts component that handles file uploads. When it rejected a malformed Content-Type, it built an error message that was then evaluated.[1], [3], [2]
Expression injection (CWE-917)- Untrusted input ends up inside an expression that an interpreter evaluates, so data becomes instructions.[6]
Asset inventory- A record of which software and versions run on which systems. Without it, a patch notice cannot be matched to the machines that need it.[4]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-20 CWE-917
- CISA KEV — added 2021-11-03 — CVE-2017-5638
Incident Timeline
Apache publishes security bulletin S2-045 and fixed Struts versions 2.3.32 and 2.5.10.1.[1]
Equifax later told GAO that a separate intrusion began on May 13 and that attackers started extracting personal data; an earlier unauthorized access had already occurred, without data being taken.[4]
Equifax notices suspicious traffic after renewing an expired certificate on a traffic-inspection device.[4]
Equifax publicly announces the breach. GAO later reports that data of at least 145.5 million U.S. individuals and nearly 1 million outside the U.S. was affected.[4]
Step-by-Step Attack Flow
A crafted Content-Type header
The attacker sends an upload request whose Content-Type header contains an OGNL expression instead of a media type.[1]
Quiet, long exfiltration
Attackers used credentials found in a data repository to expand from the portal’s three associated databases to an additional 48 unrelated databases. GAO reports about 9,000 queries and an attack lasting about 76 days; an expired certificate prevented encrypted traffic inspection.[4]
Source Code: Flaw vs. Secure Implementation
Illustrative Java, not Struts source; the real fix in Struts 2.3.32 and 2.5.10.1 changed how the multipart error was built. The hardened version uses a fixed message key and passes the header only as a positional argument, so it is formatted as text and never becomes part of an expression. It also replaces anything that does not look like a media type with a placeholder, so even a later change to the message cannot bring the header back into an evaluated string.[3], [2], [6]
import com.opensymphony.xwork2.TextProvider;
import javax.servlet.http.HttpServletRequest;
public final class UploadErrors {
private final TextProvider text; // resolves messages and evaluates expressions in them
public UploadErrors(TextProvider text) {
this.text = text;
}
public String describe(HttpServletRequest request) {
String contentType = request.getHeader("Content-Type");
// The raw header becomes part of the message that gets evaluated.
return text.getText("Invalid content type: " + contentType);
}
}
import com.opensymphony.xwork2.TextProvider;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
public final class UploadErrors {
private static final Pattern MEDIA_TYPE = Pattern.compile(
"^[a-z]+/[a-z0-9.+-]+(;\\s*[a-z0-9-]+=[a-z0-9._-]+)*$");
private final TextProvider text;
public UploadErrors(TextProvider text) {
this.text = text;
}
public String describe(HttpServletRequest request) {
String contentType = request.getHeader("Content-Type");
boolean wellFormed = contentType != null && contentType.length() <= 256
&& MEDIA_TYPE.matcher(contentType.toLowerCase(Locale.ROOT)).matches();
// A fixed message key; the header is only ever a positional argument,
// and only when it has the shape of a media type.
return text.getText("upload.invalid.content.type",
new String[] { wellFormed ? contentType : "(malformed)" });
}
}
Engineering & System Hardening Checklist
- ✓Keep a software bill of materials for every deployed application and be able to answer 'where do we run Struts 2.3.x?' within hours of an advisory such as S2-045.[4]
- ✓Reconcile vulnerability-scanner targets against the asset inventory; a clean scan of a host that was never in scope is not evidence of a patch.[4]
- ✓Alert on certificate expiry for TLS-inspection and monitoring devices at least 30 days ahead; an expired certificate hid months of exfiltration here.[4]
- ✓In code review, reject any call that passes concatenated untrusted text to a message resolver, template engine or expression evaluator; pass it as an argument instead.[6]
- ✓Segment databases from internet-facing applications, keep credentials out of application files, and alert on unusual query volume from application accounts.[4]
Sources
- Apache Struts — security bulletin S2-045 (CVE-2017-5638)
- Apache Struts — multipart parser error-message fix (commit 3523064)
- Apache Struts — upstream multipart error-handling fix (commit 6b8272c)
- U.S. GAO — Data Protection: Actions Taken by Equifax and Federal Agencies in Response to the 2017 Breach (GAO-18-559)
- CVE-2017-5638 — CVE record
- MITRE CWE-917 — Expression Language Injection
- Log4Shell: when writing to a log file became remote code execution
- Command Injection — full class reference