 |
EXPLOITS E VULNERABILIDADES |
 |
| |
Problemas com Perl CGI |
| Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 07 of 19 |
| Data |
09.09.1999 |
| Vulnerabilidade |
Poison Null Byte |
| Autor |
rain.forest.puppy (rfp @ wiretrip.net) |
| Site |
www.wiretrip.net |
| Sistemas afetados |
Perl em qualquer plataforma |
| Sinopse |
Problemas de segurança da linguagem Perl com o uso de byte NULL. Considerações e exemplos. Artigo publicado na Phrack Magazine n.55 |
|
| |
Descrição |
| A papa fina |
Nota: O nome 'Poison NULL byte' foi originalmente usado por Olaf Kirch numa publicação Bugtraq. Gostei e combina bem... Assim, usei-o. Lembranças ao Olaf.
Quando é que "root" != "root" mas, ao mesmo tempo, é "root" == "root" (Confuso agora)? Quando você mistura linguagens de programação.
Certa noite comecei a imaginar o que a linguagem Perl permitiria e se seria possível obter algo que explodisse de forma inesperada. Comecei então a fazer um pipe de dados muito malucos para várias funções e chamadas de sistema. Nada excepcional, exceto uma coisa que chamou a atenção...
Veja só. Eu quis abrir um determinado arquivo, o "rfp.db". Usei um cenário web fictício para obter um valor de entrada "rfp", grudado num ".db", e depois abri o arquivo. Em Perl, a parte funcional do script era algo como:
# desmembrar $user_input
$database="$user_input.db";
open(FILE "<$database");
Jóia. Eu passo 'user_input=rfp' e o script tenta abrir "rfp.db". Bastante simples (por enquanto vamos ignorar a tranqueira óbvia de /../). Aí a coisa começou a ficar interessante quando eu passei 'user_input=rfp%00'. Perl fez $database="rfp\0.db" e depois tentou abrir $database. O resultado? Abriu "rfp" (ou teria aberto, se existisse). O que aconteceu com o ".db"? Esta é que é a parte interessante.
Você vê que Perl permite caracteres NUL como dados nas suas variáveis. Diferentemente de C, NUL não é um caracter delimitador. Desta forma, "root" != "root\0". Porém, as chamadas ao sistema/kernel subjacente são programadas em C a qual, SIM, reconhece NUL como um delimitador. Qual é então o resultado final? Perl passa "rfp\0.db" mas as bibliotecas subjacentes param o processamento quando batem no (nosso) primeiro NUL.
O que aconteceria se tivéssemos um script que permitisse admins junior habilitados mudarem as senhas de qualquer conta EXCETO root? O código poderia ser:
$user=$ARGV[1] # usuário que o admin jr quer mudar
if ($user ne "root"){
# faça o que for necessário para este usuário
}
(**NOTA: anotado de forma simplificada e teórica apenas para ilustrar a idéia)
Bem, se o admin jr. tenta 'root' como nome, nada vai acontecer. Porém, se o admin jr. passa 'root\0', Perl obtém sucesso no teste e executa o bloco. Agora, quando as chamadas do sistema são canalizadas para fora (a não ser que tudo seja feito em Perl, o que é possível mas pouco provável), este NUL será efetivamente descartado e as ações acontecerão no registro do root.
Apesar disso não ser necessariamente um problema de segurança em si, definitivamente é uma característica interessante a ser observada. Tenho visto muitas CGIs que pregam um ".html" em alguns formulários de dados enviados por usuários para obter a página resultante. Isto é
page.cgi?page=1
se transforma e me mostra 1.html. Semi-seguro, porque adiciona ".html" à página, de modo que você pensa: menos mal, vou mostrar apenas páginas HTML. Bem, se enviarmos a coisa como
page.cgi?page=page.cgi%00 (%00 == '\0' com escape)
então o script acabará nos devolvendo uma cópia do seu próprio código! Até mesmo uma checagem com o '-e' do Perl irá falhar:
$file="/etc/passwd\0.txt.seja.lá.o.que.queremos";
die("hahaha! Te peguei!) if($file eq "/etc/passwd");
if (-e $file){
open (FILE, ">$file");
}
Isto funcionará (se de fato existir um /etc/passwd) e abrirá o arquivo para escrita.
Solução? Simples! Remova os NULs. Em Perl é tão simples como
$dados_inseguros=~s/\0//g;
Nota: não faça o escape junto com o restante dos metacaracteres de shell. Remova-os completamente. |
| |
Corte e Queime |
Se você der uma olhada no FAQ de Segurança do W3C WWW, você verá que a lista recomendada de metacaracteres de shell é:
&;`'\"|*?~<>^()[]{}$\n\r
O que eu acho mais interessante é que todo mundo parece esquecer da contra-barra ('\'). Talvez seja apenas o jeito que você precisa escrever o código de escape no Perl:
s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g;
Com todos estes escapes de contra-barra [](){}, etc, fica confuso certificar-se de que a contra-barra também esteja sendo levada em consideração (aqui ela é '\\'). Talvez algumas pessoas sejam apenas disléxicos-regex (disléxicos em expressões regulares) e pensem que, vendo uma instância de contra-barra, então ela deve estar sendo tratada.
Está bem, porque isto é importante? Imagine que você tenha a seguinte linha sendo submetida ao seu CGI:
dados do usuário `rm -rf /`
Você a passa através do seu código de escape Perl, que a transforma em: dados do usuário \`rm -rf /\`
o que agora é seguro para operações em shell, etc. Agora, digamos que você tenha esquecido de eliminar as contra-barras com escape. O usuário envia a seguinte linha:
dados do usuário \`rm -rf / \`
Seu código muda-a para:
dados do usuário \\`rm -rf / \\`
As contra-barras em duplicidade serão transformadas em uma única contra-barra de 'dados' deixando as crases ( `) sem escape. O que efetivamente será executado é `rm -rf / \`. Claro que, com este método, você sempre terá que lidar com contra-barras espúreas. Deixando a contra-barra como o último caracter da linha fará com que a Perl cause erros nas chamadas do sistema e das crases (pelo menos, no meu teste aconteceu). Você precisa ser esperto para contornar a situação. ;) (É possível...)
Outro efeito colateral interessante da contra-barra vem do seguinte código feito para prevenir transposições reversas de diretório:
s/\.\.//g;
Tudo o que é feito é remover os pontos duplos, amassando a transposição reversa de um arquivo. Assim,
/usr/tmp/../../etc/passwd
será transformado em
/usr/tmp///etc/passwd
que não funciona (Nota: barras múltiplas são permitidas. Experimente -l /etc////passwd')
Agora, entra nossa amiga contra-barra. Vamos dar-lhe a linha
/usr/tmp/.\./.\./etc/passwd
e a expressão regular (regex) não vai bater devido à presença da contra-barra. Agora, faça uso deste nome de arquivo em Perl
file="/usr/tmp/.\\./.\\./etc/passwd";
$file=s/\.\.//g;
system("ls -l $file");
Nota: precisamos usar contra-barras duplas para conseguir que a Perl insira apenas uma contra-barra como 'dado' -- de outra forma, a Perl assume que você está apenas fazendo escape de pontos. Como dado, a string ainda continua sendo "/usr/tmp/.\./.\./etc/passwd".
Entretanto, o código acima apenas funciona em chamadas ao sistema e com crases (backticks). O '-e' e as funções open (non-piped) da Perl NÃO funcionam. Portanto:
$file="/usr/tmp/.\\./.\\./etc/passwd";
open(FILE, "<$file") or die("Arquivo inexistente");
vai morrer (die) com "Arquivo inexistente". Meu palpite é que é porque a shell é necessária para processar a '\' em '.' (uma vez que um ponto com escape continua sendo apenas um ponto). Solução? Certifique-se de ter feito o escape da contra-barra. Simples o bastante. |
| |
O chato do pipe |
Em Perl, anexando um '|' (pipe) no final de um nome de arquivo em uma declaração open, faz com que a Perl execute o arquivo especificado ao invés de abrí-lo. Desta forma,
open(FILE, "/bin/ls")
vai te devolver um monte de código binário, mas
open(FILE, "/bin/ls|")
vai executar /bin/ls. Note que a seguinte regex
s/(\|)/\\$1/g
previne isto (Perl morre com um 'unexpected end of file', porque sh fica querendo a próxima linha indicada pela '\'. Se você encontrar um meio de contornar isto, me avise).
Agora podemos tornar a situação mais complexa com outras técnicas que acabamos de aprender. Suponhamos que $FORM seja uma entrada crua submetida pelo usuário ao CGI. Primeiramente temos:
open(FILE, "$FORM")
onde podemos passar $FORM para "ls|" para obter a listagem do diretório. Agora, suponha que tivéssemos:
$filename="/safe/dir/to/read/$FORM"
open(FILE, $filename)
então precisamos indicar especificamente onde "ls" está, então passamos $FORM para "../../../../bin/ls|", o que nos dá a listagem do diretório. Uma vez que isto é um piped open, nossa técnica de contra-barra para burlar regex anti-transposição reversa possivelmente poderá ser usada, se for aplicável.
A partir deste ponto podemos usar opções de linhas de comando com comandos. Por exemplo, usando o trecho de código acima, podemos passar $FORM para "touch /myself|" para criar o arquivo /myself (perdão, não pude resistir ao nome de arquivo. :)
A seguir, temos uma situação um pouco mais difícil:
$filename="/safe/dir/to/read/$FORM"
if(!(-e $filename)) die("Nem pensar!")
open(FILE, $filename)
Agora precisamos enganar o '-e'. O problema é que o '-e' vai voltar como não existe se ele tentar encontrar 'ls|' porque está procurando pelo nome de arquivo com um pipe no final. Desta forma, precisamos 'remover' o pipe para a checagem do '-e' mas precisamos mantê-lo para que o Perl possa 'vê-lo'. Teve alguma idéia? Poison NULL (o NULL venenoso) é a salvação! Tudo o que precisamos fazer é passar $FORM para "ls\0|" (ou, num formulário web GET com escape, "ls%00|"). Isto faz com que o '-e' cheque "ls" (ele pára de processar no nosso NUL, ignorando o pipe). A Perl, no entanto, ainda vê o pipe quando chegar a hora de abrir nosso arquivo, de modo que executará nosso comando. Entretanto, existe um porém... quando a Perl executa nosso comando, ela pára no nosso NULL -- significa que não podemos especificar opções de linha de comando. Talvez alguns exemplos ilustrem melhor:
$filename="/bin/ls /etc|"
open(FILE, $filename)
Isto fornece a listagem do diretório /etc.
$filename="/bin/ls /etc\0|"
if(!(-e $filename)) exit;
open(FILE, $filename)
Isto força a saída porque o '-e' vê que "/bin/ls /etc" não existe.
$filename="/bin/ls\0 /etc|"
if(!(-e $filename)) exit;
open(FILE, $filename)
Isto vai funcionar, porém vamos apenas obter a listagem do nosso diretório atual (um 'ls' raso)... não vai fornecer o '/etc' para ls como um argumento.
Também quero dar um recado para vocês, os relaxados: se vocês, programadores Perl preguiçosos (não *TODOS* programadores Perl; apenas os preguiçosos) se dessem ao trabalho de usar a cabeça e indicar um modo de abertura de arquivo específico, acabavam de vez com esta discussão.
$bug="ls|"
open(FILE, $bug)
open(FILE, "$bug")
funciona. Porém
open(FILE, "<$bug")
open(FILE, ">$bug")
open(FILE, ">>$bug") etc..etc..
não funciona. Desta forma, se você quiser fazer a leitura de um arquivo, então abra "<$file" e não apenas $file. Inserindo este sinal de 'menor que' (apenas um caracter insignificante!) pode salvar você e seu servidor de um monte de aflições.
Ok, agora que possuímos algumas armas, vamos enfrentar o inimigo. |
| |
Scripts Pearl (inseguros) na vida real |
| Script gerenciador de anúncios classificados v.1.1 de Dan Bloomquist |
Nosso primeiro CGI eu tirei do freecode.com. É um script gerenciador de anúncios classificados. Do arquivo de CGI:
# First version 1.1
# Dan Bloomquist dan@lakeweb.net
Agora, nosso primeiro exemplo... Dan desmembra todas as variáveis de entrada em %DATA. Ele não remove '..' nem caracteres NUL. Bem, então vamos dar uma espiada num trecho de código:
#This sets the real paths to the html and lock files.
#It is done here, after the POST data is read.
#of the classified page.
$pageurl= $realpath . $DATA{ 'adPath' } . ".html";
$lockfile= $realpath . $DATA{ 'adPath' } . ".lock";
Usando 'adPath=/../../../../../etc/passwd%00' podemos especificar que $pageurl aponte para o arquivo /etc/passwd. O mesmo vale para o $lockfile. Não podemos usar o pipe anexado porque ele anexa o ".html"/".lock" no fim (bem, você PODE usá-lo, mas ele não irá funcionar. ;)
#Read in the classified page
open( FILE,"$pageurl" ) || die "can't open to read $pageurl: $!\n";
@lines= <FILE>;
close( FILE );
Aqui o Dan faz a leitura em $pageurl, que é o arquivo que especificamos. Felizmente para Dan, ele abre imediatamente após o $pageurl para escrita. Deste modo, seja lá o que especificamos para leitura, também precisa de direitos para a escrita. Isto limita o potencial do exploit, mas serve como um bom exemplo deste tipo de problema na vida real.
De forma bastante interessante, Dan continua com:
#Send your mail out.
#
open( MAIL, "|$mailprog $DATA{ 'adEmail' }" ) || die "can't open sendmail: $adEmail: $!\n";
Hmmmmm... este é o nosso padrão na-na-ni-na-não. E Dan não desmembra metacaracteres de shell, de modo que 'adEmail' se torna bastante frágil. |
| Formulário para logar de Leif M. Wright |
Continuando a sapear no freecode.com eu consegui um formulário simples para logar:
# flexform.cgi
# Written by Leif M. Wright
# leif@conservatives.net
Leif desmembra o input do formulário para %contents e não faz o escape dos metacaracteres da shell. Depois ele faz
$output = $basedir . $contents{'file'};
open(RESULTS, ">>$output");
Usando nosso atravessador de diretório reverso padrão nós nem mesmo temos que anular uma extensão com NUL. Seja qual for o arquivo que for indicado, ele será aberto para append e, novamente, precisamos ter um pouco de sorte com nossas permissões. Outra vez, nosso bug do pipe não vai funcionar porque ele determinou especificamente o modo append (através do '>>'). |
| LWGate v.1.16 de David W. Baker |
O próximo é o LWGate, o qual é uma interface WWW de vários pacotes de listas de correspondência (mailing list) muito populares.
# lwgate by David W. Baker, dwb@netspace.org #
# Version 1.16 #
Dave pões variáveis desmembradas de formulário em %CGI. Depois temos
# The mail program we pipe data to
$temp = $CGI{'email'};
$temp =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
$MAILER = "/usr/sbin/sendmail -t -f$temp"
open(MAIL,"| $MAILER") || &ERROR('Error Mailing Data')
Hmmmm... Dave parece ter esquecido a contra-barra na sua substituição de regex. Nada bom. |
| Perlshop v.3.1 da ARPnet Corp. |
Ok, vamos pular para um dos muitos aplicativos de carrinho de compras. Este, novamente, foi arrancado do freecode.com - o Perlshop.
$PerlShop_version = 3.1;
# A product of ARPAnet Corp. -
perlshop@arpanet.com, www.arpanet.com/perlshop
A parte interessante é:
open (MAIL, "|$blat_loc - -t $to -s $subject")
|| &err_trap("Can't open $blat_loc!\n")
$to é obviamente o email definido pelo usuário. Blat é um programa de mail NT. Lembre-se de que os metacaracteres no NT são <>&|% (talvez mais?).
Você se lembra do problema do chato do pipe que eu mencionei? (Espero que você se lembre... Foi apenas a alguns parágrafos!). Admito, é um bug muito improvável, mas eu o achei. Vamos seguir em frente com o Matt's Script Archive. |
| File Download v.1.0 de Matthew M. Wright |
# File Download Version 1.0
# Copyright 1996 Matthew M. Wright mattw@worldwidemart.com
Primeiro ele desmembra os dados de entrada do usuário em $Form (não fazendo escape de nada). Depois ele roda o seguinte:
$Request_File = $BASE_DIR . $Form{'s'} . '/' . $Form{'f'};
if (!(-e $filename)) {
&error('File Does Not Exist');
}
elsif (!(-r $filename)) {
&error('File Permissions Deny Access');
}
open(FILE,"$Request_File");
while (<FILE>) {
print;
}
Isto preenche os requisitos para o 'problema do chato do pipe' (tm). Temos a checagem do '-e', por isso não vamos usar argumentos na linha de comando. Uma vez que ele gruda $BASE_DIR na frente, vai ser necessário usar a transposição de diretório reversa.
Tenho certeza de que olhando o código acima você (deveria) ver um problema muito mais simples. E se f=../../../../../../etc/passwd? Bem, se existir e permitir a leitura, ele vai ser mostrado. E pasme, ele é mostrado. Uma nota adicional: todos os acessos ao download.cgi são logados pelo seguinte código:
open(LOG,">>$LOG_FILE");
print LOG "$Date|$Form{'s'}|$Form{'c'}|$Form{'f'}\n";
close(LOG);
Você vai estar na mira da câmera enquanto estiver agindo. Mas, de qualquer modo, você não deveria estar fazendo coisas deste tipo no servidor de outras pessoas. ;) |
| BigNoseBird v.2.2 da BigNoseBird.com |
Vamos pular para o BigNoseBird.com. O script que eu estou pensado:
bnbform.cgi
#(c)1997 BigNoseBird.Com
# Version 2.2 Dec. 26, 1998
O código de interesse está após o script abrir um pipe para o sendmail como MAIL:
if ($fields{'automessage'} ne "") {
open (AM,"< $fields{'automessage'}");
while (<AM>) {
chop $_;
print MAIL "$_\n";
}
Este é outro muito simples. BNB não faz nenhum desmembramento das variáveis de entrada do usuário (em $fields) por isso podemos especificar qualquer arquivo que quisermos para 'automessage'. Assumindo que permita a leitura pelo contexto do servidor web, ele será enviado para qualquer endereço que indicarmos (pelo menos é o que a teoria diz). |
| |
Comentários interessantes |
| b0iler em 09.01.02 |
Apesar deste tutorial ter me ensinado como fazer um exploit em perl, achei uma falha nele.
Quando ele fala sobre s/\.\.//g não ser suficiente, ele apenas menciona o uso de .\\./.\\./ quando na verdade existe outro modo (possivelmente mais modos). Assim, quando ele diz: "Solução? Certifique-se de ter feito o escape da contra-barra. Simples o bastante." não é tão simples assim. Tome como exemplo esta string: .../...//.../...// agora s/\.\.//g e s/\\//g fariam dela ../../ O que você precisa é substituir todos os pontos (.) se possível ou então tentar um loop como: while($value =~ /\.\./g){ $value =~ /\.\./g; } e, claro, $value =~ s/\\//g;
Apesar disso, um texto dos diabos. |
| xtype em 28.02.02 |
Hmmmm... ao invés de tentar remover tudo que não queremos numa string, deveríamos remover tudo EXCETO o que queremos que o CGI manipule.
Para os exemplos acima: $file =~ s/[^a-zA-Z\d]//g; |
| |
Declaração |
| Do site NumaBoa |
O site NumaBoa apenas traduziu o material publicado pela Phrack e não pode ser considerada responsável pelo uso ou efeitos potenciais destes programas ou avisos, nem do conteúdo que possuam. Deveriam servir apenas para programadores para que possam aumentar o nível de segurança dos seus scripts CGI. O uso indevido das informações é de exclusiva responsabilidade de quem faça uso das mesmas. |
|