How To Remove Meta-characters From User-Supplied Data In CGI Scripts
This file: ftp://ftp.cert.org/pub/tech_tips/cgi_metacharacters ║ ftp://ftp.cert.org/pub/tech_tips/cgi_metacharacters
Last revised November 13, 1997
Version 1.3
---------------------------------------------------------------
1. Definition of the Problem
We have noticed several reports to us and to public mailing lists about CGI
scripts that allow an attacker to execute arbitrary commands on a WWW
server under the effective user-id of the server process.
In many of these cases, the author of the script has not sufficiently
sanitized user-supplied input.
2. Definition of "Sanitize"
Consider an example where a CGI script accepts user-supplied data. In
practice, this data may come from any number of sources of user-supplied
data; but for this example, we will say that the data is taken from an
environment variable $QUERY_STRING. The manner in which data was inserted
into the variable is not important - the important point here is that the
programmer needs to gain control over the contents of the data in
$QUERY_STRING before further processing can occur. The act of gaining this
control is called "sanitizing" the data.
3. A Common But Inadvisable Approach
A script writer who is aware of the need to sanitize data may decide to
remove a number of well-known meta-characters from the script and replace
them with underscores. A common but inadvisable way to do this is by
removing particular characters.
For instance, in Perl:
#!/usr/local/bin/perl
$user_data = $ENV{'QUERY_STRING'}; # Get the data
print "$user_data\n";
$user_data =~ s/[\/ ;\[\]\≤\≥&\t]/_/g; # Remove bad characters. WRONG!
print "$user_data\n";
exit(0);
In C:
#include ≤stdio.h≥
#include ≤string.h≥
#include ≤stdlib.h≥
int
main(int argc, char *argv[], char **envp)
{
static char bad_chars[] = "/ ;[]≤≥&\t";
char * user_data; /* our pointer to the environment string */
char * cp; /* cursor into example string */
/* Get the data */
user_data = getenv("QUERY_STRING");
printf("%s\n", user_data);
/* Remove bad characters. WRONG! */
for (cp = user_data; *(cp += strcspn(cp, bad_chars)); /* */)
*cp = '_';
printf("%s\n", user_data);
exit(0);
}
In this method, the programmer determines which characters should NOT be
present in the user-supplied data and removes them. The problem with this
approach is that it requires the programmer to predict all possible inputs.
If the user uses input not predicted by the programmer, then there is the
possibility that the script may be used in a manner not intended by the
programmer.
4. A Recommended Approach
A better approach is to define a list of acceptable characters and replace any
character that is NOT acceptable with an underscore. The list of valid input
values is typically a predictable, well-defined set of manageable size. For
example, consider the tcp_wrappers package written by Wietse Venema. In the
percent_x.c module, Wietse has defined the following:
char *percent_x(...)
{
{...}
static char ok_chars[] = "1234567890!@%-_=+:,./\
abcdefghijklmnopqrstuvwxyz\
ABCDEFGHIJKLMNOPQRSTUVWXYZ";
{...}
for (cp = expansion; *(cp += strspn(cp, ok_chars)); /* */ )
*cp = '_';
{...}
The benefit of this approach is that the programmer is certain that
whatever string is returned, it contains only characters now under his or her
control.
This approach contrasts with the approach we discussed earlier. In the earlier
approach, which we do not recommend, the programmer must ensure that he or she
traps all characters that are unacceptable, leaving no margin for error. In
the recommended approach, the programmer errs on the side of caution and only
needs to ensure that acceptable characters are identified; thus the programmer
can be less concerned about what characters an attacker may try in an attempt
to bypass security checks.
Building on this philosophy, the Perl program we presented above could be
thus sanitized to contain ONLY those characters allowed. For example:
#!/usr/local/bin/perl
$_ = $user_data = $ENV{'QUERY_STRING'}; # Get the data
print "$user_data\n";
$OK_CHARS='-a-zA-Z0-9_.@'; # A restrictive list, which
# should be modified to match
# an appropriate RFC, for example.
s/[^$OK_CHARS]/_/go;
$user_data = $_;
print "$user_data\n";
exit(0);
Likewise, the same updated example in C:
#include ≤stdio.h≥
#include ≤string.h≥
#include ≤stdlib.h≥
int
main(int argc, char *argv[], char **envp)
{
static char ok_chars[] = "abcdefghijklmnopqrstuvwxyz\
ABCDEFGHIJKLMNOPQRSTUVWXYZ\
1234567890_-.@";
char * user_data; /* our pointer to the environment string */
char * cp; /* cursor into example string */
user_data = getenv("QUERY_STRING");
printf("%s\n", user_data);
for (cp = user_data; *(cp += strspn(cp, ok_chars)); /* */)
*cp = '_';
printf("%s\n", user_data);
exit(0);
}
5. Recommendation
We strongly encourage you to review all CGI scripts available via your web
server to ensure that any user-supplied data is sanitized using the approach
described in Section 4, adapting the example to meet whatever specification
you are using (such as the appropriate RFC).
6. Additional Tips
The following comments appeared in CERT Advisory CA-97.12 "Vulnerability in
webdist.cgi" and AUSCERT Advisory AA-97.14, "SGI IRIX webdist.cgi
Vulnerability."
We strongly encourage all sites should consider taking this opportunity
to examine their entire httpd configuration. In particular, all CGI
programs that are not required should be removed, and all those
remaining should be examined for possible security vulnerabilities.
It is also important to ensure that all child processes of httpd are
running as a non-privileged user. This is often a configurable option.
See the documentation for your httpd distribution for more details.
Numerous resources relating to WWW security are available. The
following pages may provide a useful starting point. They include
links describing general WWW security, secure httpd setup, and secure
CGI programming.
The World Wide Web Security FAQ:
http://www-genome.wi.mit.edu/WWW/faqs/www-security-faq.html ║ http://www-genome.wi.mit.edu/WWW/faqs/www-security-faq.html
The following book contains useful information including sections on
secure programming techniques.
_Practical Unix & Internet Security_, Simson Garfinkel and
Gene Spafford, 2nd edition, O'Reilly and Associates, 1996.
Please note that the CERT/CC and AUSCERT do not endorse the URL that
appears above. If you have any problem with the sites, please contact
the site administrator.
Another resource that sites can consider is the CGI.pm module. Details
about this module are available from:
http://www.genome.wi.mit.edu/ftp/pub/software/WWW/cgi_docs.html ║ http://www.genome.wi.mit.edu/ftp/pub/software/WWW/cgi_docs.html
This module provides mechanisms for creating forms and other web-based
applications. Be aware, however, that it does not absolve the programmer
from the safe-coding responsibilities discussed above.
Copyright 1997 Carnegie Mellon University. Conditions for use, disclaimers,
and sponsorship information can be found in
http://www.cert.org/legal_stuff.html and ftp://info.cert.org/pub/legal_stuff .
If you do not have FTP or web access, send mail to cert@cert.org with
"copyright" in the subject line.
CERT is registered in the U.S. Patent and Trademark Office.
* УЧИМСЯ ПИСАТЬ БЕЗОПАСНЫЕ CGI-СКРИПТЫ *
---------------------------------------------------------------
СТАТЬЯ - 11 / 11 / 99 *
Author: Rain Forest Puppy, http://www.wiretrip.net ║ http://www.wiretrip.net
Оригинал статьи лежит на сайте Team Void.Ru ║ http://www.void.ru/stat/9911/11.html
© Copyright Rain Forest Puppy
© Copyright Team Void, перевод
---------------------------------------------------------------
УЧИМСЯ ПИСАТЬ БЕЗОПАСНЫЕ CGI-СКРИПТЫ
Данный текст - статья из журнала Phrack #55. Автор статьи - .Rain.forest.puppy., с чьего любезного разрешения Team Void и представляет Вам этот материал. Сильно рекомендуется ознакомиться с данным материалом всем писателям на перле - ведь из-за мелкой ошибки в скрипте, который вы пишете по заказу клиента, он может стать жертвой злых хакеров и предъявить Вам иск ;)
# parse $user_input $database="$user_input.db"; open(FILE "<$database");
$user=$ARGV[1] #
if ($user ne "root"){
# осуществить дальнейшие операции }
Итак, если теперь некто, пользующийся этим скриптом скажет - поменять паpоль у пользователя root - то ничего не произойдёт. Если же он поменяется поменять пароль у пользователя root\0 = то тест будет пройден, но все вызовы, которые делает часть скрипта, меняющая пароль, будут считать что операция выполняется над пользователем root.
$file="/etc/passwd\0.txt.whatever.we.want";
die("hahaha! Caught you!) if($file eq "/etc/passwd");
if (-e $file){
open (FILE, ">$file");}
откроет файл /etc/passwd для чтения. Решение проблемы - очень порстое - просто отфильтруйте все нулевые символы : $insecure_data=~s/\0//g;
$file="/usr/tmp/.\\./.\\./etc/passwd";
$file=s/\.\.//g;
system("ls -l $file");
Но всё вышеописанное работает лишь на системных вызовах и вызовах, заключенных в кавычки (''). Опция -e не позволит открытым (non-piped) функциям работать.
$file="/usr/tmp/.\\./.\\./etc/passwd";
open(FILE, "<$file") or die("No such file");
завершит работу с сообщением NO SUCH FILE.
$filename="/safe/dir/to/read/$FORM" open(FILE, $filename)Мы должны точно указать директорию, где находится ls - посему мы устанавливаем $FROM в "../../../../bin/ls|", что даёт нам листинг директории.
$filename="/safe/dir/to/read/$FORM"
if(!(-e $filename)) die("I don't think so!")
open(FILE, $filename)
Требуется одурачить опцию -e. Проблема в том, что "-е" прекратит работу фонкции, как только удостоверится, что файл, который надо открыть, заканчивается пайпом (|). Итак - нам надо сделать пайп невидимым для -е, но добиться того, чтобы перл по прежнему знал о его сущуствовании. Для этого мы будем использовать тот самый нулевой байт. Итак - мы открываем что-то вроде ls%00| , -е оканчивает обработку данный на нулевом байте, и команда ls исполняется!
$filename="/bin/ls /etc\0|" if(!(-e $filename)) exit; open(FILE, $filename)не выдаст нам содержимое папки /etc. Это происходит потому, что -e видит, что /bin/ls /etc не существует. Правильный вариант будет выглядеть так:
$filename="/bin/ls\0 /etc|" if(!(-e $filename)) exit; open(FILE, $filename)Это сработает, но мы получим листинг текущей папки - (простое ls) - /etc не воспринимается как аргумент.
$bug="ls|" open(FILE, $bug) open(FILE, "$bug")сработает. Но
# First version 1.1 # Dan Bloomquist dan@lakeweb.netАвтор передаёт все параметры, с которыми был вызван скрипт в %DATA. Он не очищает '..', не очишает и нулевые байты. Итак, поглядим код...
#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";
Используя 'adPath=/../../../../../etc/passwd%00' - мы можем указать $pageurl на файл с паролями. Посмотрим на $lockfile. Мы не можем использовать пайп для наших нужд - так как расширение (.html) добавляется в последнюю очередь.
#Read in the classified page open( FILE,"$pageurl" ) || die "can't open to read $pageurl: $!\n"; @lines= ; close( FILE );Тут в $pageurl заносится имя файла, коорый затем открывается. К счастью для автора, он немедленно окрывает $pageurl на запись. Итак, мы должны иметь права на запись в то, что, вообще-то пытаемся открыть на чтение. Это ограничивает наши возможности использования данной уязвимости, но это есть живой пример того, как сам того не зная, автор сильно связал нам руки в раскалывании серверов через его скрипт.
#Send your mail out.
#
open( MAIL, "|$mailprog $DATA{ 'adEmail' }" )
|| die "can't open sendmail: $adEmail: $!\n";
Ох.. это мы видели уже сто раз. Не проверяются ни символы перенаправления потока данных, ни пайпы.. мы можем выполнить любую команду через это.
# flexform.cgi # Written by Leif M. Wright # leif@conservatives.netИтак, входные данные передаются в %CONTENTS, и опять никакой поверки на подозрительные символы. Затем
$output = $basedir . $contents{'file'};
open(RESULTS, ">>$output");
Используя стандартный выход за пределы папки, куда нас посадили (/../../) , нам даже не придётся использовать нефильтрацию 0x00. Но что бы мы не открыли - файл открывается для дописывания, и поэтому мы должны иметь права на запись в тот файл, который пытаемся открыть. По этой же причине не сработает и pipe (|) bug.