FlawOpen

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

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Crítica
Pesquisa · FlawOpen

Equifax: violação expôs dados de quase 150 milhões

A CVE-2017-5638 permitiu injetar OGNL pelo cabeçalho Content-Type no portal Struts sem correção da Equifax. O ataque durou cerca de 76 dias e expôs dados de quase 150 milhões de pessoas.[1], [4]

💡 Explicação em Linguagem Simples (ELI5)

Imagine um sistema de correspondência que transforma uma etiqueta de envio rejeitada em um aviso de erro. Se o aviso tratar a etiqueta como um modelo de instruções executável, o remetente pode fazer o sistema seguir instruções em vez de apenas informar que o endereço é inválido. O Struts já tinha uma correção, mas o aviso da Equifax foi enviado a uma lista desatualizada de administradores e uma varredura da rede não detectou a vulnerabilidade no portal.[1], [4]

Conceitos Centrais e Termos

OGNL
Object-Graph Navigation Language, a linguagem de expressões que o Struts usa para ler e alterar propriedades de objetos Java. Avaliar texto do atacante como OGNL é executar o código do atacante.[1], [6]
Parser multipart Jakarta
O componente do Struts que trata uploads de arquivos. Ao rejeitar um Content-Type malformado, ele montava uma mensagem de erro que depois era avaliada.[1], [3], [2]
Injeção de expressão (CWE-917)
Uma entrada não confiável acaba dentro de uma expressão que um interpretador avalia, e os dados viram instruções.[6]
Inventário de ativos
Um registro de qual software e qual versão roda em cada sistema. Sem ele, não dá para associar um aviso de patch às máquinas que precisam dele.[4]

Mapeamentos de frameworks

Linha do Tempo do Incidente

2017-03-07

A Apache publica o boletim de segurança S2-045 e as versões corrigidas 2.3.32 e 2.5.10.1 do Struts.[1]

2017-05-13

A Equifax informou posteriormente à GAO que uma nova intrusão começou em 13 de maio e que os atacantes passaram a extrair dados pessoais; um acesso não autorizado anterior já havia ocorrido, sem roubo de dados.[4]

2017-07-29

A Equifax percebe tráfego suspeito depois de renovar um certificado vencido num equipamento de inspeção de tráfego.[4]

2017-09-07

A Equifax anuncia publicamente a violação. Mais tarde, a GAO informa que dados de pelo menos 145,5 milhões de pessoas nos EUA e quase 1 milhão fora dos EUA foram afetados.[4]

Fluxo de Ataque Passo a Paso

Passo 1

Um cabeçalho Content-Type forjado

O atacante envia uma requisição de upload cujo cabeçalho Content-Type contém uma expressão OGNL no lugar de um tipo de mídia.[1]

Passo 2

A mensagem de erro carrega o cabeçalho

O parser Jakarta rejeita o cabeçalho e monta uma mensagem de erro localizada que inclui o texto bruto do cabeçalho.[1], [3], [2]

Passo 3

A mensagem é avaliada

O Struts avalia expressões dentro dessa mensagem, então o OGNL do atacante roda como código Java com os privilégios da aplicação web.[1], [3], [2], [6]

Passo 4

Exfiltração lenta e silenciosa

Os atacantes usaram credenciais encontradas em um repositório para ampliar o acesso dos três bancos de dados do portal a outros 48 bancos de dados sem relação com o portal. A GAO relata cerca de 9.000 consultas e um ataque de aproximadamente 76 dias; um certificado vencido impedia a inspeção do tráfego criptografado.[4]

Código-Fonte: Vulnerável vs. Seguro

Java ilustrativo, não é código-fonte do Struts; a correção real nas versões 2.3.32 e 2.5.10.1 mudou a forma de montar o erro do multipart. A versão reforçada usa uma chave de mensagem fixa e passa o cabeçalho só como argumento posicional, então ele é formatado como texto e nunca vira parte de uma expressão. Ela também troca por um marcador qualquer valor que não pareça um tipo de mídia, de modo que nem uma mudança futura na mensagem consegue devolver o cabeçalho a uma string avaliada.[3], [2], [6]

IMPLEMENTAÇÃO VULNERÁVEL
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");
        // O cabeçalho bruto passa a fazer parte da mensagem que será avaliada.
        return text.getText("Invalid content type: " + contentType);
    }
}
PATCH SEGURO E ROBUSTO
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();
        // Use uma chave de mensagem fixa; o cabeçalho é apenas um argumento posicional,
        // e somente quando tiver o formato de um tipo de mídia.
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

Lista de Verificação de Segurança para Engenharia

Fontes