FlawOpen

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

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Kritisch
Sicherheitsforschung · FlawOpen

Equifax: Daten von fast 150 Millionen offengelegt

CVE-2017-5638 ermöglichte OGNL-Injection über den Content-Type-Header im ungepatchten Struts-Portal von Equifax. Der Angriff dauerte etwa 76 Tage und legte Daten von fast 150 Millionen Menschen offen.[1], [4]

💡 Einfache Erklärung (ELI5)

Stellen Sie sich eine Versandsoftware vor, die ein abgelehntes Paketetikett in eine Fehlermeldung übernimmt. Behandelt die Meldung das Etikett als ausführbare Vorlage, kann der Absender die Software Anweisungen ausführen lassen, statt nur eine ungültige Adresse zu melden. Für Struts gab es bereits einen Patch, doch Equifax versandte den Hinweis an eine veraltete Administratorenliste und ein Netzwerkscan erkannte die Schwachstelle im Portal nicht.[1], [4]

Kernkonzepte & Begriffe

OGNL
Object-Graph Navigation Language, die Ausdruckssprache, mit der Struts Eigenschaften von Java-Objekten liest und setzt. Angreifertext als OGNL auszuwerten heißt, den Code des Angreifers auszuführen.[1], [6]
Jakarta-Multipart-Parser
Die Struts-Komponente für Datei-Uploads. Lehnte sie einen fehlerhaften Content-Type ab, baute sie eine Fehlermeldung, die anschließend ausgewertet wurde.[1], [3], [2]
Ausdrucks-Injection (CWE-917)
Nicht vertrauenswürdige Eingaben landen in einem Ausdruck, den ein Interpreter auswertet – Daten werden zu Anweisungen.[6]
Asset-Inventar
Eine Aufstellung, welche Software in welcher Version auf welchem System läuft. Ohne sie lässt sich ein Patch-Hinweis nicht den betroffenen Maschinen zuordnen.[4]

Framework-Zuordnungen

Vorfall-Zeitstrahl

2017-03-07

Apache veröffentlicht das Sicherheitsbulletin S2-045 und die korrigierten Struts-Versionen 2.3.32 und 2.5.10.1.[1]

2017-05-13

Equifax teilte dem GAO später mit, dass am 13. Mai ein separater Einbruch begann und die Angreifer ab dann personenbezogene Daten extrahierten; ein früherer unbefugter Zugriff war bereits erfolgt, ohne Daten zu stehlen.[4]

2017-07-29

Equifax bemerkt verdächtigen Datenverkehr, nachdem ein abgelaufenes Zertifikat auf einem Gerät zur Verkehrsprüfung erneuert wurde.[4]

2017-09-07

Equifax macht den Vorfall öffentlich. Das GAO berichtet später, dass Daten von mindestens 145,5 Millionen Menschen in den USA und knapp einer Million außerhalb der USA betroffen waren.[4]

Schritt-für-Schritt Angriffsablauf

Schritt 1

Ein präparierter Content-Type-Header

Der Angreifer sendet eine Upload-Anfrage, deren Content-Type-Header statt eines Medientyps einen OGNL-Ausdruck enthält.[1]

Schritt 2

Die Fehlermeldung übernimmt den Header

Der Jakarta-Parser lehnt den Header ab und erzeugt eine lokalisierte Fehlermeldung, die den rohen Headertext enthält.[1], [3], [2]

Schritt 3

Die Meldung wird ausgewertet

Struts wertet Ausdrücke in dieser Meldung aus, sodass das OGNL des Angreifers als Java-Code mit den Rechten der Webanwendung läuft.[1], [3], [2], [6]

Schritt 4

Leiser, langer Datenabfluss

Die Angreifer nutzten in einem Datenspeicher gefundene Zugangsdaten, um von den drei Portal-Datenbanken auf 48 weitere, vom Portal unabhängige Datenbanken zuzugreifen. Das GAO berichtet von etwa 9.000 Abfragen und einem rund 76 Tage dauernden Angriff; ein abgelaufenes Zertifikat verhinderte die Prüfung verschlüsselten Datenverkehrs.[4]

Quellcode: Verwundbar vs. Sicher

Illustratives Java, kein Struts-Quellcode; die echte Korrektur in 2.3.32 und 2.5.10.1 änderte, wie der Multipart-Fehler gebaut wird. Die gehärtete Version nutzt einen festen Meldungsschlüssel und übergibt den Header nur als Positionsargument, sodass er als Text formatiert und nie Teil eines Ausdrucks wird. Außerdem ersetzt sie alles, was nicht wie ein Medientyp aussieht, durch einen Platzhalter – selbst eine spätere Änderung der Meldung kann den Header so nicht wieder in eine ausgewertete Zeichenkette bringen.[3], [2], [6]

VERWUNDBARE IMPLEMENTIERUNG
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");
        // Der unverarbeitete Header wird Teil der ausgewerteten Nachricht.
        return text.getText("Invalid content type: " + contentType);
    }
}
GEHÄRTETER SICHERHEITS-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();
        // Einen festen Nachrichtenschlüssel verwenden; der Header ist nur ein Positionsargument,
        // und nur, wenn er die Form eines Medientyps hat.
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

Checkliste für Engineering & Systemsicherheit

Quellen