Manipulação de tipos: escapando de sandboxes de templates
21 de março de 2017
0 minutos de leituraUma característica importante de linguagens interpretadas, como JavaScript e Ruby, é a tipagem dinâmica, na qual os tipos das variáveis são determinados e atualizados em tempo de execução. A tipagem dinâmica tem suas desvantagens, mas pode tornar o software mais flexível e acelerar o desenvolvimento. Infelizmente, ela abre caminho para um vetor de ataque chamado manipulação de tipos, no qual atacantes tentam modificar o tipo de uma variável e provocar um comportamento inesperado.
Este é o primeiro de uma série de posts sobre manipulação de tipos. Cada um apresenta uma ou mais vulnerabilidades reais que podem ser exploradas por meio da manipulação de tipos e explica como poderiam ter sido evitadas. Os exemplos reais servem tanto para alertar você sobre vulnerabilidades específicas em pacotes que talvez esteja usando quanto para ajudar você a aprender com esses erros e evitá-los no seu próprio código.
Neste post, vamos mostrar como a manipulação de tipos pode contornar sandboxes de frameworks de templates. Esses templates costumam incluir mecanismos de proteção, mas — como veremos em breve — eles nunca são perfeitos. Nossos exemplos se baseiam em vulnerabilidades nas plataformas Dust.js, do LinkedIn, e Nunjucks, da Mozilla, com uma breve menção ao Angular.
Vulnerabilidade de execução remota de comandos no Dust.js, do LinkedIn
Nosso principal exemplo será uma vulnerabilidade no Dust.js, um framework popular de templates publicado e usado pelo LinkedIn. O PayPal também usa o Dust.js, e foi lá que um pesquisador descobriu uma vulnerabilidade de execução remota de comandos explorada por meio da manipulação de tipos.
A vulnerabilidade foi divulgada em 9 de janeiro de 2015 e corrigida na versão 2.6.0, publicada em 14 de setembro de 2016. Se você usa Dust.js, não deixe de verificar se seus aplicativos usam a versão vulnerável.
Dependência explícita do tipo
Como a maioria das bibliotecas de templates, o Dust.js oferece suporte a condições nos templates, como mostra o exemplo a seguir, que usa o parâmetro device para escolher qual HTML renderizar:
Essas condições são avaliadas posteriormente usando a função eval(), uma maneira simples de oferecer suporte a condições altamente complexas no template. Veja um trecho da função if do Dust.js, em que os params fornecidos pelo usuário e o cond estático fornecido pelo desenvolvedor são combinados para criar o valor a ser avaliado:
Como a condição pode conter dados fornecidos pelo usuário (por exemplo, o parâmetro device), o Dust.js usa uma função de sanitização para impedir que valores maliciosos de parâmetros sejam avaliados como código. Veja um trecho dessa função:
O código de sanitização é claro e sólido, e captura todos os caracteres conhecidos até então que permitem sair da string entre aspas. Além disso, a chamada HCHARS.test(s) só é executada em strings, provavelmente para evitar erros com valores indefinidos.
Embora possa evitar erros, essa verificação de tipo é justamente o ponto que abre espaço para um ataque de manipulação de tipos. Se o atacante conseguir fazer com que s seja um array (ou qualquer objeto que não seja uma string), poderá contornar completamente a verificação. Mais adiante no código, o array será convertido implicitamente em uma string quando a condição for passada para eval, permitindo a execução remota de código JavaScript.
De string para array
O próximo passo do atacante é tentar manipular o tipo da variável. Em aplicativos orientados a API, ele pode tentar fazer isso modificando um payload JSON, como vimos no caso da vulnerabilidade do Buffer do Mongoose. Mas, como esta é uma plataforma de templates para web, vamos nos concentrar na manipulação de tipos por meio do qs.
O pacote qs é o pacote JavaScript mais usado para analisar strings de consulta e vem sendo usado por padrão no express, no request e em outros pacotes populares. O qs converte uma string de consulta em um objeto JavaScript, facilitando seu uso. Veja alguns exemplos comuns de uso do qs:
Como você pode notar, o qs deduz o tipo do objeto criado com base na forma como ele aparece na string de consulta. Se o mesmo nome aparecer várias vezes, ele será representado como um array. Da mesma forma, se um parâmetro for explicitamente declarado como array, esse será o tipo atribuído a ele na análise.
Falta pouco para explorar a vulnerabilidade do Dust.js. No template de exemplo acima, que foi usado de fato pelo PayPal, basta o atacante fornecer o argumento device duas vezes ou passá-lo como um parâmetro device[]=value para que a variável seja transmitida como um array, contornando a lógica de sanitização.
Pós-exploração do Dust.js
Embora fuja um pouco do tema da manipulação de tipos, é interessante ver como um atacante pode tirar proveito dessa vulnerabilidade por meio das chamadas ações de “pós-exploração”.
Primeiro, o atacante precisa fazer com que o código seja executado. Para isso, pode fornecer uma URL como esta:
Registrar uma mensagem no console não é muito útil para um atacante. Por isso, ele pode fornecer um script mais elaborado, por exemplo, para enviar algumas informações. No caso da exploração no PayPal, o pesquisador usou um payload como este:
Isso resulta em uma chamada a eval como esta:
A pós-exploração usa o módulo child_process para executar localmente um comando curl e enviar o arquivo /etc/passwd ao atacante. Com um payload um pouco mais longo, isso também poderia ter sido feito com um comando nativo do Node. Mas o uso de child_process eleva a vulnerabilidade de execução remota de código JS para execução remota de comandos shell, ampliando as possibilidades do atacante.
Opções de correção
A maioria dos ataques de manipulação de tipos — e este não é exceção — pode ser resolvida rejeitando, normalizando ou tratando de forma personalizada o tipo da variável. Essas três opções podem ser combinadas para criar várias camadas de defesa.
Rejeitar significa aceitar apenas tipos específicos. Neste caso, o Dust.js poderia ter rejeitado parâmetros que não fossem strings em seus templates. Isso reduziria a funcionalidade; portanto, a viabilidade depende de como o framework é usado.
Normalizar significa converter vários tipos de entrada para um único tipo. Neste caso, o Dust.js convertia implicitamente as variáveis em questão para uma string ao passá-las à função eval(). Em vez de converter para string nesse ponto, funções anteriores poderiam converter um array, inteiro ou outro tipo recebido em uma string e processar o restante do código — incluindo a sanitização — sabendo que apenas strings são permitidas.
Por fim, o tratamento personalizado consiste em criar um tratamento específico para todos os tipos permitidos. Neste caso, o Dust.js poderia identificar que a entrada é um array e normalizar cada caractere, levando em conta também as formas perigosas de combinar vários itens do array. Essa é a opção mais frágil das três, mas também permite lidar com entradas mais complexas.
O Dust.js optou por uma forma de tratamento personalizado que tenta capturar todos os objetos que têm uma função toString. Essa solução resolve o problema, mas deixa certa fragilidade no código, pois o código subsequente pode converter um array em uma saída para o usuário de outra maneira. Veja o trecho relevante do patch do Dust.js:
Se você usa uma versão vulnerável do Dust.js e não pode atualizá-la por algum motivo, pode normalizar no seu código os tipos de entrada passados ao Dust.js ou rejeitar tipos que não sejam strings, bloqueando arrays maliciosos. Você pode verificar se está usando uma versão vulnerável com a CLI ou a integração com o GitHub da Snyk.
Vulnerabilidade de XSS no Mozilla Nunjucks
A vulnerabilidade do Dust.js não é um caso isolado, mas um padrão que pode se repetir. Para ver um exemplo, vamos analisar uma oportunidade semelhante de manipulação de tipos que existia em outro framework de templates: a biblioteca Nunjucks, da Mozilla. Essa vulnerabilidade foi divulgada em 6 de setembro de 2016 por Matt Austin e corrigida na versão 2.4.3, publicada em 9 de setembro de 2016. Assim como no caso do Dust.js, você pode verificar se seus aplicativos usam uma versão vulnerável do Nunjucks com a Snyk.
Assim como Handlebars, Mustache e outros, o Nunjucks permite especificar nomes de variáveis entre duas chaves (``) para indicar que o valor entre elas deve ser codificado em HTML. Veja um exemplo dessa proteção em ação:
No entanto, assim como no Dust.js, a função de sanitização do Nunjucks escapava apenas parâmetros do tipo string. Veja um trecho do código de sanitização:
Seguindo a mesma lógica do exemplo acima, uma URL como esta (analisada pelo qs):
Vai resultar em uma chamada e em uma saída XSS como esta:
Como você pode ver, o problema segue o mesmo padrão: o código não prevê que o atacante possa manipular o tipo. Como resultado, o atacante pode contornar a sanitização e injetar código malicioso.
Para corrigir o problema, assim como no caso do Dust.js, o Nunjucks poderia rejeitar entradas em formato de array, normalizá-las para string ou aplicar um tratamento de sanitização personalizado também para arrays. Como mostra este patch, a Mozilla optou por normalizar as entradas em formato de array usando o método toString.
Resumo
A manipulação de tipos é um vetor de ataque menos conhecido, mas representa um perigo real para todas as linguagens de tipagem dinâmica. Nessas linguagens, precisamos nos acostumar a tratar o tipo como outra forma de entrada e aceitar apenas tipos autorizados, normalizá-los ou criar um tratamento personalizado para cada tipo.
Neste post, vimos como a manipulação de tipos pode ser usada para escapar da sandbox de um framework de templates — um problema que afetou vários frameworks. Criar uma sandbox sem controlar o ambiente de execução é extremamente difícil, por isso o Angular acabou abandonando essa abordagem.
Em posts futuros, vamos mostrar outros cenários em que a manipulação de tipos pode entrar em jogo, ajudando você a entender melhor os riscos que ela apresenta e como pode se manifestar.


