FlawOpen

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

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Критический
Исследования · FlawOpen

Equifax: данные почти 150 млн человек раскрыты

CVE-2017-5638 позволяла внедрять OGNL через заголовок Content-Type на не обновлённом портале Equifax на Struts. Атака длилась около 76 дней и раскрыла данные почти 150 миллионов человек.[1], [4]

💡 Простыми словами (ELI5)

Представьте систему доставки, которая копирует отклонённую почтовую этикетку в сообщение об ошибке. Если сообщение воспринимает этикетку как исполняемый шаблон инструкций, отправитель может заставить систему выполнить указания вместо простого сообщения о неверном адресе. Для Struts уже был патч, но уведомление Equifax отправили по устаревшему списку администраторов, а сканирование сети не выявило уязвимость на портале.[1], [4]

Ключевые понятия и термины

OGNL
Object-Graph Navigation Language — язык выражений, которым Struts читает и задаёт свойства Java-объектов. Вычислить текст злоумышленника как OGNL значит выполнить его код.[1], [6]
Парсер multipart Jakarta
Компонент Struts, обрабатывающий загрузку файлов. Отклоняя некорректный Content-Type, он формировал сообщение об ошибке, которое затем вычислялось.[1], [3], [2]
Внедрение выражений (CWE-917)
Недоверенный ввод попадает в выражение, которое вычисляет интерпретатор, и данные становятся инструкциями.[6]
Реестр активов
Учёт того, какое ПО и какой версии работает на каких системах. Без него уведомление о патче невозможно сопоставить с машинами, которым он нужен.[4]

Связи со стандартами

Хронология инцидента

2017-03-07

Apache публикует бюллетень безопасности S2-045 и исправленные версии Struts 2.3.32 и 2.5.10.1.[1]

2017-05-13

Позднее Equifax сообщила GAO, что отдельное вторжение началось 13 мая и тогда злоумышленники приступили к извлечению персональных данных; ранее уже был несанкционированный доступ, но данные тогда не похищали.[4]

2017-07-29

Equifax замечает подозрительный трафик после продления истёкшего сертификата на устройстве проверки трафика.[4]

2017-09-07

Equifax публично объявляет об утечке. Позднее GAO сообщает, что были затронуты данные как минимум 145,5 миллиона человек в США и почти миллиона за пределами страны.[4]

Пошаговый сценарий атаки

Шаг 1

Подделанный заголовок Content-Type

Злоумышленник отправляет запрос на загрузку, в заголовке Content-Type которого вместо типа содержимого стоит выражение OGNL.[1]

Шаг 2

Сообщение об ошибке несёт заголовок

Парсер Jakarta отклоняет заголовок и формирует локализованное сообщение об ошибке, включающее его исходный текст.[1], [3], [2]

Шаг 3

Сообщение вычисляется

Struts вычисляет выражения в этом сообщении, поэтому OGNL злоумышленника выполняется как Java-код с правами веб-приложения.[1], [3], [2], [6]

Шаг 4

Тихий и долгий вынос данных

Злоумышленники использовали найденные в хранилище учётные данные, чтобы перейти от трёх связанных с порталом баз к ещё 48 не связанных с порталом базам. По данным GAO, они выполнили около 9 000 запросов; атака длилась примерно 76 дней, а просроченный сертификат мешал проверять зашифрованный трафик.[4]

Исходный код: Уязвимый vs Защищённый вариант

Иллюстративный Java-код, а не исходники Struts; настоящее исправление в 2.3.32 и 2.5.10.1 изменило способ формирования ошибки multipart. Защищённая версия использует фиксированный ключ сообщения и передаёт заголовок только как позиционный аргумент, поэтому он форматируется как текст и никогда не становится частью выражения. Кроме того, всё, что не похоже на тип содержимого, заменяется заглушкой, так что даже будущее изменение сообщения не вернёт заголовок в вычисляемую строку.[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");
        // Необработанный заголовок становится частью вычисляемого сообщения.
        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();
        // Используйте фиксированный ключ сообщения; заголовок передаётся только как позиционный аргумент,
        // и только если он имеет формат типа содержимого.
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

Чек-лист по защите системы для инженеров

Источники