FlawOpen

flawopen.com/Incidents/equifax-struts-ognl-rce-july-2017

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Critical
FlawOpen Security Research

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]

💡 Plain English Explainer (ELI5)

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

Incident Timeline

2017-03-07

Apache publishes security bulletin S2-045 and fixed Struts versions 2.3.32 and 2.5.10.1.[1]

2017-05-13

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]

2017-07-29

Equifax notices suspicious traffic after renewing an expired certificate on a traffic-inspection device.[4]

2017-09-07

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

Step 1

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]

Step 2

The error message carries the header

The Jakarta parser rejects the header and builds a localised error message that includes the raw header text.[1], [3], [2]

Step 3

The message is evaluated

Struts evaluates expressions in that message, so the attacker's OGNL runs as Java code with the web application's privileges.[1], [3], [2], [6]

Step 4

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]

UNPATCHED FLAW
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);
    }
}
HARDENED SECURE PATCH
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

Sources