Atualmente, muitos incidentes de segurança na web envolvem automação. Ataques de web scraping, reutilização de senhas e fraude de cliques são perpetrados por adversários que tentam imitar usuários reais e, assim, tentam fazer parecer que estão vindo de um navegador. Como proprietário de um site, você quer garantir que atenderá humanos e, como provedor de serviços web, você quer que o acesso programático ao seu conteúdo passe pela sua API em vez de ser passado pela sua interface web mais pesada e menos estável.
Supondo que você tenha verificações básicas para visitantes do tipo cURL, o próximo passo razoável é garantir que os visitantes estejam usando navegadores reais, baseados em interface de usuário — e não navegadores sem interface como PhantomJS e SlimerJS .
Neste artigo, demonstraremos algumas técnicas para identificar visitas do PhantomJS. Decidimos focar no PhantomJS porque é o ambiente de navegador headless mais popular, mas muitos dos conceitos que abordaremos são aplicáveis ao SlimerJS e outras ferramentas.
Observação: As técnicas apresentadas neste artigo são aplicáveis tanto ao PhantomJS 1.x quanto ao 2.x, a menos que explicitamente mencionado. Primeiro: é possível detectar o PhantomJS sem nem mesmo responder a ele?
Como você deve saber, o PhantomJS é construído no framework Qt . A maneira como o Qt implementa a pilha HTTP o destaca de outros navegadores modernos.
Primeiro, vamos dar uma olhada no Chrome, que envia os seguintes cabeçalhos:
No PhantomJS, no entanto, a mesma solicitação HTTP se parece com isto:
Você notará que os cabeçalhos do PhantomJS são diferentes do Chrome (e, como se vê, de todos os outros navegadores modernos) de algumas maneiras sutis:
Ao verificar essas aberrações de cabeçalho HTTP no servidor, deve ser possível identificar um navegador PhantomJS.
Mas é seguro acreditar nesses valores? Se um adversário usar um proxy para reescrever cabeçalhos na frente do navegador headless, ele poderá modificar esses cabeçalhos para que pareçam um navegador moderno normal.
Parece que resolver esse problema apenas no servidor não é uma solução mágica. Então, vamos dar uma olhada no que pode ser feito no cliente, usando o ambiente JavaScript do PhantomJS.
Talvez não possamos confiar no valor do User-Agent entregue via HTTP, mas e no cliente?
Infelizmente, é igualmente trivial alterar os valores do cabeçalho user-agent e navigator.userAgent no PhantomJS, então isso pode não ser suficiente.
navigator.plugins contém uma variedade de plugins que estão presentes no navegador. Os valores típicos de plugins incluem Flash, ActiveX, suporte para applets Java e o “ Default Browser Helper ”, que é um plugin que indica se este navegador é o navegador padrão no OS X. Em nossa pesquisa, a maioria das novas instalações de navegadores comuns inclui pelo menos um plugin padrão — mesmo em dispositivos móveis.
Isso é diferente do PhantomJS, que não implementa nenhum plugin, nem fornece uma maneira de adicionar um (usando a API do PhantomJS ).
A seguinte verificação pode ser útil:
Por outro lado, é bastante trivial falsificar esse conjunto de plugins modificando o ambiente JavaScript do PhantomJS antes que a página seja carregada .
Também não é difícil imaginar uma construção personalizada do PhantomJS com plugins reais e implementados. Isso é mais fácil do que parece porque o framework Qt no qual o PhantomJS é construído fornece uma API nativa para implementar plugins.
Outro ponto de interesse é como o PhantomJS suprime diálogos JavaScript:
Após diversas medições, parece que se o diálogo de alerta for suprimido em 15 milissegundos, o navegador provavelmente não está sendo controlado por um humano. Mas usar essa abordagem significa incomodar usuários reais com um alerta que eles terão que fechar manualmente.
O PhantomJS 1.x expõe duas propriedades no objeto global:
No entanto, essas propriedades são parte de uma característica experimental e podem mudar no futuro.
Atualmente, o PhantomJS 1.x e 2.x usam mecanismos WebKit desatualizados, o que significa que há recursos de navegador existentes em navegadores mais novos que não existem no PhantomJS. Isso se estende ao mecanismo JavaScript — onde algumas propriedades e métodos nativos são diferentes ou ausentes no PhantomJS.
Um desses métodos é o Function.prototype.bind, que não está disponível no PhantomJS 1.x e versões anteriores. O exemplo a seguir verifica se o bind está presente e se ele não foi falsificado no ambiente de execução.
Este código é um pouco complicado para ser explicado em detalhes aqui, mas você pode descobrir mais em nossa apresentação .
Erros gerados pelo código JavaScript avaliado pelo PhantomJS por meio do comando assess contêm um rastreamento de pilha exclusivamente identificável, a partir do qual podemos identificar o navegador headless.
Por exemplo, suponha que o PhantomJS chame a avaliação no seguinte código:
Observe que este exemplo usa uma função indexOfString() personalizada, deixada como um exercício para o leitor, já que o String.prototype.indexOf nativo pode ser falsificado pelo PhantomJS para sempre retornar um resultado negativo.
Agora, como você faz um script PhantomJS avaliar esse código? Uma técnica é substituir algumas funções da API DOM usadas com frequência que provavelmente serão chamadas. Por exemplo, o código abaixo substitui document.querySelectorAll para inspecionar o rastreamento de pilha do navegador:
Neste artigo, examinamos 7 técnicas diferentes para identificar o PhantomJS, tanto no servidor quanto executando código no ambiente JavaScript do cliente do PhantomJS. Ao combinar os resultados da detecção com um forte mecanismo de feedback — por exemplo, tornando uma página dinâmica inerte ou invalidando o cookie da sessão atual — você pode introduzir um obstáculo sólido para os visitantes do PhantomJS. No entanto, tenha sempre em mente que essas técnicas não são infalíveis e que um adversário sofisticado acabará conseguindo passar.
Para saber mais, recomendamos assistir a esta gravação da nossa apresentação na AppSec USA 2014 ( slides ). Também reunimos um repositório no GitHub de exemplos de implementações — e possíveis evasões — das técnicas apresentadas aqui.
Obrigado pela leitura e boa caça.
Sergey Shekyan – @sshekyan
Ben Vinagre – @bentlegen
Bei Zhang – @ikarienator