FlawOpen

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

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Critique
Recherche · FlawOpen

Equifax : données de près de 150 millions exposées

CVE-2017-5638 permettait une injection OGNL via l’en-tête Content-Type sur le portail Struts non corrigé d’Equifax. L’attaque a duré environ 76 jours et a exposé les données de près de 150 millions de personnes.[1], [4]

💡 Explication en Langage Simple (ELI5)

Imaginez un logiciel de courrier qui transforme une étiquette d’expédition rejetée en message d’erreur. Si ce message interprète l’étiquette comme un modèle d’instructions actif, l’expéditeur peut faire exécuter ses instructions au logiciel au lieu de signaler une adresse invalide. Struts avait déjà un correctif, mais l’avis d’Equifax a été envoyé à une liste d’administrateurs obsolète et une analyse réseau n’a pas détecté la vulnérabilité sur le portail.[1], [4]

Concepts Clés et Termes

OGNL
Object-Graph Navigation Language, le langage d'expressions qu'utilise Struts pour lire et modifier les propriétés d'objets Java. Évaluer du texte d'attaquant en OGNL revient à exécuter son code.[1], [6]
Parseur multipart Jakarta
Le composant de Struts qui gère l'envoi de fichiers. Quand il rejetait un Content-Type mal formé, il construisait un message d'erreur qui était ensuite évalué.[1], [3], [2]
Injection d'expression (CWE-917)
Une entrée non fiable se retrouve dans une expression qu'un interpréteur évalue : les données deviennent des instructions.[6]
Inventaire des actifs
Un registre des logiciels et versions présents sur chaque système. Sans lui, impossible de rapprocher un avis de correctif des machines concernées.[4]

Correspondances de référentiels

Chronologie de l'Incident

2017-03-07

Apache publie le bulletin de sécurité S2-045 et les versions corrigées 2.3.32 et 2.5.10.1 de Struts.[1]

2017-05-13

Equifax a indiqué plus tard au GAO qu’une nouvelle intrusion avait commencé le 13 mai et que les attaquants avaient alors commencé à extraire des données personnelles ; un accès non autorisé antérieur avait déjà eu lieu, sans vol de données.[4]

2017-07-29

Equifax repère un trafic suspect après avoir renouvelé un certificat expiré sur un équipement d'inspection du trafic.[4]

2017-09-07

Equifax annonce publiquement la violation. Le GAO indique ensuite que les données d’au moins 145,5 millions de personnes aux États-Unis et de près d’un million à l’étranger ont été concernées.[4]

Déroulement de l'Attaque Étape par Étape

Étape 1

Un en-tête Content-Type forgé

L'attaquant envoie une requête d'envoi de fichier dont l'en-tête Content-Type contient une expression OGNL au lieu d'un type de média.[1]

Étape 2

Le message d'erreur embarque l'en-tête

Le parseur Jakarta rejette l'en-tête et construit un message d'erreur localisé qui reprend son texte brut.[1], [3], [2]

Étape 3

Le message est évalué

Struts évalue les expressions de ce message : l'OGNL de l'attaquant s'exécute comme du code Java avec les privilèges de l'application web.[1], [3], [2], [6]

Étape 4

Une exfiltration lente et discrète

Les attaquants ont utilisé des identifiants trouvés dans un dépôt pour passer des trois bases associées au portail à 48 autres bases sans lien avec le portail. Le GAO rapporte environ 9 000 requêtes et une attaque d’environ 76 jours ; un certificat expiré empêchait l’inspection du trafic chiffré.[4]

Code Source : Vulnérable vs Sécurisé

Java illustratif, pas du code source Struts ; le vrai correctif des versions 2.3.32 et 2.5.10.1 a modifié la construction de l'erreur multipart. La version durcie utilise une clé de message fixe et ne transmet l'en-tête que comme argument positionnel : il est mis en forme comme du texte et n'entre jamais dans une expression. Elle remplace aussi par un marqueur toute valeur qui ne ressemble pas à un type de média, si bien qu'une modification ultérieure du message ne peut pas réintroduire l'en-tête dans une chaîne évaluée.[3], [2], [6]

IMPLÉMENTATION VULNÉRABLE
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");
        // L’en-tête brut est intégré au message qui sera évalué.
        return text.getText("Invalid content type: " + contentType);
    }
}
PATCH SÉCURISÉ ET ROBUSTE
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();
        // Utiliser une clé de message fixe ; l’en-tête n’est qu’un argument positionnel,
        // et seulement s’il a la forme d’un type MIME.
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources