Por  Rafael Carneiro Reis de Souza

Neste artigo vamos abordar um pouco sobre a vulnerabilidade, Web cache poisoning (Envenenamento de web caches).

Como o nome diz, o web cache poisoning é uma vulnerabilidade relacionada com caches, mais particularmente os web caches. Então vamos primeiro entender melhor o que são caches e como eles funcionam.

Os Caches são muito utilizados em aplicações web, com o objetivo de aumentar o desempenho da aplicação. Os web caches ficam entre o cliente e o servidor da aplicação e fazem o armazenamento de conteúdos requisitados, recentemente, por um cliente. Desta forma, em uma próxima requisição para o mesmo conteúdo, o serviço de cache pode entregar os dados sem a necessidade de “incomodar” o servidor da aplicação, diminuindo a latência da resposta e também a demanda para aquele servidor.

A imagem abaixo mostra um exemplo do funcionamento de um web cache:

Figura 1 – Funcionamento de um web cache. Fonte: Próprio autor

Os conteúdos de aplicações web podem ser armazenados em vários tipos de cache, como caches de navegadores e cache de um servidor intermediário. No exemplo da imagem anterior, como também nos próximos exemplos deste artigo, os conteúdos são armazenados em um cache intermediário, da forma de um proxy reverso.

Todo conteúdo armazenado em um cache tem um tempo de vida. Este tempo de vida ocorre devido à necessidade dos caches de renovarem os arquivos que foram armazenados, para que eles sejam atualizados caso o servidor os tenha modificado.

Os web caches sabem como e quando devem armazenar um conteúdo a depender dos cabeçalhos que são recebidos junto com a resposta. Estes cabeçalhos compõem a política que os serviços de cache devem seguir ao armazenar o conteúdo recebido. Eles são:

  • Expires: O expires indica o tempo de vida de um conteúdo. Seu valor indica o tempo no futuro em que o conteúdo não será mais válido. Após esse valor, todos os caches que armazenam esse conteúdo devem pedir ao servidor um novo documento a fim de renová-lo.
  • Cache-Control: Esse é o principal cabeçalho a respeito de caches. Nele temos várias diretrizes de controle de cache, o qual nos informa se um conteúdo deve ser armazenado em cache; o tempo de vida que um cache deve armazenar esse conteúdo; como também o local onde o cache deve ser armazenado. As suas principais diretrizes são:
    • no-store: informa que o conteúdo não deve ser armazenado em nenhum cache, de forma alguma.
    • no-cache: nesta diretiva o conteúdo é armazenado em cache, mas pergunta ao servidor se deve ser entregue o conteúdo em cache ou o conteúdo provido pelo servidor.
    • public: indica que o conteúdo pode ser armazenado em caches compartilhados.
    • private: indica que o conteúdo só pode ser armazenado no navegador do usuário.
    • max-age: semelhante ao Expires indica o tempo de vida que um conteúdo deve ter ao ser armazenado em cache.
    • must-revalidate: indica que um conteúdo deve necessariamente ser revalidado caso o tempo no max-age ou no Expire tiverem sido expirados.
    • Age: o cabeçalho age infere o tempo em segundos que o conteúdo está armazenado no cache. Em conjunto com o max-age do cache-control ele ajuda a notarmos quanto tempo falta para o conteúdo expirar.
    • X-Cache; CF-Cache-Status e afins: estes tipos de cabeçalho existem em caches compartilhados e eles indicam quem entregou o conteúdo recebido. Caso o seu valor seja hit o conteúdo foi entregue pelo serviço de cache e em caso de miss o conteúdo foi entregue pelo servidor web da aplicação.
    • Vary: neste cabeçalho são especificados os campos da requisição que irão fazer com que a resposta varie, logo seu valor deve ser incluído na chave de cache.

Os web caches utilizam uma estratégia chamada de chave de cache para ter conhecimento de quais conteúdos eles já possuem armazenados. Esta estratégia consiste em utilizar alguns campos da requisição para constituírem uma chave e esta chave indica se o conteúdo já está armazenado ou não em cache.

Essa abordagem facilita no gerenciamento de quais documentos estão armazenados no serviço de cache, pois apenas os campos que são essenciais para identificar se uma requisição é igual a outra são utilizados. Para exemplificar, imagine que toda a requisição fosse utilizada para identificá-la, dessa forma, alguns campos que não modificam o conteúdo que será recebido na resposta irão ser levados em conta. Um desses campos é o User-Agent. Este cabeçalho apenas indica qual navegador está fazendo a requisição e, para a maioria das aplicações, a resposta não muda a depender do navegador.

Na prática, o que acontecerá é que o serviço de cache armazenará para cada navegador a mesma resposta de uma requisição, diminuindo no desempenho dos serviços de cache. Para fugir desse tipo de cenário, a abordagem de chave de cache foi adotada. Um exemplo é mostrado na imagem a seguir, na qual os campos usados na chave de cache estão em vermelho:

Figura 2 – Exemplo de uma possível chave de cache. Fonte: Próprio autor

Bom, agora que temos conhecimento do que são caches e como eles funcionam, podemos começar a falar dos perigos que podem ocorrer se não os utilizarmos de forma correta.

Web cache poisoning

A vulnerabilidade de web cache poisoning traz a possibilidade de utilizar os serviços de cache para entregar páginas maliciosas aos clientes de um website. Ela consiste em identificar a possibilidade de gerar uma página maliciosa e depois em utilizar o serviço de cache da aplicação para que os seus clientes recebam a página maliciosa.

Porque ela acontece?

Ela pode acontecer através de outras vulnerabilidades, como HTTP splitting e HTTP request smuggling, mas a forma que iremos abordar se dá através da manipulação de componentes não pertencentes a chave de cache. Vamos explorar o fato de haver uma má configuração da chave.

Para explicar como essa vulnerabilidade acontece, podemos primeiro pensar em um cenário em que ao fazer testes em uma aplicação foi encontrado um cross site scripting (XSS) em um dos cabeçalhos. Incrível, temos um XSS, mas se formos pensar no seu impacto, ele é nulo, pois qual seria a forma de afetar um usuário daquele site? Um phishing que consiga fazer ele interceptar a requisição e adicionar o header com um payload malicioso? Totalmente inviável. Então temos que pensar em uma forma de conseguir atingir a vítima e é aí que entra a web cache poisoning, como dissemos anteriormente, temos o objetivo de utilizar o cache para entregar conteúdos maliciosos.

A ideia é utilizar de campos que não pertencem à chave do cache e que de alguma forma conseguem ocasionar um comportamento malicioso na aplicação. Mas, por que utilizar componentes que não pertencem a chave? A resposta é simples, porque se eles não estão contidos nela, o cache irá guardar a resposta da requisição com o payload malicioso para uma chave que será igual ao de uma requisição comum.

No exemplo a seguir temos o cabeçalho X-Forwarded-Host da requisição sendo refletido na tag meta da resposta, como podemos ver na imagem:

Figura 3 – Conteúdo do cabeçalho X-Forwarded-Host refletido na resposta da requisição. Fonte: Próprio autor.

Também podemos notar que a página pode estar sendo armazenada em cache e que o serviço é o CloudFront, na imagem a seguir temos um Hit do serviço de cache que confirma que o conteúdo está sendo armazenado.

Figura 4 – Retorno da requisição informando que houve um Hit do web cache. Fonte: Próprio autor.

Nesta aplicação, a chave de cache é composta pelos componentes: método HTTP; cabeçalho Host; e caminho para o objeto requisitado. Então, a resposta da segunda imagem e que continha o cabeçalho refletido foi armazenada para a chave “GET:/login/:d3umq8j94nknda.cloudfront.net”, que é a mesma chave de uma requisição feita legitimamente pelo navegador, logo a mesma resposta será entregue pelo serviço de cache, independente do header ser colocado ou não. A imagem anterior indica a resposta recebida do cache com o conteúdo contido em X-Forwarded-Host, mesmo sem o uso dele.

Como dito anteriormente, foi inferida a presença de um XSS neste cabeçalho, então o que falta é conseguir entregá-lo a uma vítima. Para isso, só precisamos utilizar o que discutimos até agora: enviar um payload no momento certo, para que a nossa requisição seja a primeira após a expiração do conteúdo em cache. A imagem a seguir mostra a requisição com o payload malicioso:

Figura 5 – Conteúdo malicioso inserido no cabeçalho X-Forwarded-Host. Fonte: Próprio autor

Como podemos ver, o payload apareceu na resposta e obtivemos um “Miss”, se enviarmos uma requisição sem o cabeçalho de “X-Forwarded-Host”, receberemos a mesma página, como mostrado abaixo.

Figura 6 – Conteúdo malicioso retornado na resposta provida pela web cache. Fonte: Próprio autor.

Agora, qualquer usuário que acessar a página irá receber o código em Javascript. A imagem a seguir ilustra uma vítima acessando a página inicial da aplicação:

Figura 7 – Execução do JavaScript após acessar a página afetada. Fonte: Próprio autor.

GETs com body

Essa vulnerabilidade não se limita a apenas headers (cabeçalhos), mas a qualquer campo da requisição que mude a resposta e não está contida na chave de cache.

Os serviços de cache não guardam em sua memória respostas que foram feitas com determinados métodos, como o POST, mas o que pode acontecer se um corpo for enviado em uma requisição GET? O serviço de cache vai armazenar a resposta desse pedido normalmente. Como o corpo de um GET não faz parte da chave de cache, qualquer mudança que o corpo faça na resposta, vai fazer com que o cache a armazene como se fosse uma requisição GET normal.

Para exemplificar temos uma aplicação em que a página de login aceita requisições GET com um corpo . Inicialmente, temos a seguinte resposta da aplicação.

Figura 8 – Requisição para a página inicial da aplicação. Fonte: Próprio autor.

A imagem a seguir mostra o comportamento da aplicação para uma requisição GET com body:

Figura 9 – Comportamento ao realizar o login utilizando o método GET. Fonte: Próprio autor.

No caso, o servidor entende como se fosse uma tentativa de login, mas como essa requisição é um GET, o conteúdo retornado ficará salvo na memória do cache para a chave com método “GET”, caminho da requisição “/polls/” e cabeçalho host “fat-get.rafael-ts.link”. O status do cache é expired nesse caso, sendo um comportamento parecido com o miss, mas informando que o que estava salvo, anteriormente, foi expirado:

Na imagem a seguir, vemos que uma requisição GET para a página de login não é mais possível, pois agora o cache entrega a página que indica que as credenciais estão erradas. Podemos notar que o status do cache agora é de HIT.

Figura 10 – Requisição para a página inicial da aplicação retornando um wrong credentials. Fonte: Próprio autor.

Nesse exemplo foi possível fazer um ataque de Denial of Service (DoS) na página de login, pois qualquer usuário que entrar nessa página receberá a resposta que contém “wrong credentials” (credenciais erradas) e não poderá completar seu acesso à aplicação.

Recomendações

Essa vulnerabilidade, em específico quando ela acontece por componentes não chaveados, é ocasionada pela má configuração do serviço de cache. Então, antes de tudo é preciso entender bem quais componentes modificam a resposta da aplicação e incluir todos eles na chave, ou apenas deixar de utilizá-los.

Alguns frameworks utilizam headers como o “X-Forwarded-Host” por padrão, então, também é preciso analisar se o framework a ser utilizado contém algum componente indesejado. Para facilitar na procura destes componentes existe uma extensão do Burp Suite feita por James Kettle, chamada ParaMiner. Nela é possível fazer uma procura por campos não chaveados da aplicação, inclusive GETs com body.

Se optarmos por fazer a análise sem a utilização de ferramentas de terceiros, devemos ter alguns cuidados ao automatizar. Quando escolhemos por automatizar a procura, podemos ter uma falsa ideia de que a resposta não está sendo modificada, mas, na verdade, o que estamos recebendo é a resposta que está no cache e não uma resposta do servidor, nesse caso o que devemos fazer é a utilização de um cache buster, ou seja, devemos tentar sempre forçar que o cache retorne um miss sem modificar a resposta, uma forma de fazer isso é utilizando parâmetros: se a aplicação utilizar parâmetros da URL como parte da chave de cache, podemos colocar qualquer valor aleatório para ser um parâmetro e assim não modificar a resposta, mas criar uma nova entrada no serviço de cache, como por exemplo:

GET /?buster=buster HTTP/1.1
Host: example.com

Para a automação é importante ter, para cada requisição, um valor aleatório diferente, caso contrário a resposta também ficará armazenada no cache.

Conclusão

Vimos neste artigo como utilizar os serviços de web cache para criar oportunidades de ataques, então é sempre bom verificar se estes serviços estão bem configurados. As possibilidades são muito variadas e os ataques, mostrados aqui, só são exemplos de como pode acontecer, pois como o web cache poisoning pode ser utilizado a favor de um atacante, dependendo da aplicação.

Referências

Cache poisoning in popular open source packages | Snyk Blog. Disponível em: <https://snyk.io/blog/cache-poisoning-in-popular-open-source-packages/>.

Cache Poisoning Software Attack | OWASP Foundation. Disponível em: <https://owasp.org/www-community/attacks/Cache_Poisoning>.

HTTP caching – HTTP | MDN. Disponível em: <https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Caching>.

Practical Web Cache Poisoning. Disponível em: <https://portswigger.net/research/practical-web-cache-poisoning>.

Prevent unnecessary network requests with the HTTP Cache. Disponível em: <https://developers.google.com/web/fundamentals/performance/get-started/httpcaching-6>.

Web Caching Basics: Terminology, HTTP Headers, and Caching Strategies. Disponível em: <https://www.digitalocean.com/community/tutorials/web-caching-basics-terminology-http-headers-and-caching-strategies>.