Портал для веб-мастера
Вход пользователей
Поиск статей
WoWeb.ru » Статьи » Программирование для Web » PERL/CGI

Проблемы CGI на Perl

Введение

Мне кажется, что я должен немного пояснить о чем будет идти речь. По большей части я сам писал и просматривал различные CGI и пытался разобраться с тем, как убрать те несколько проблем, которые, по-моему, являются дырками в системе. На этом я закруглюсь перейду к дыркам.

Полуфабрикат

Ядовитый NULL байт

Обратите внимание: название `Poison NULL byte` изначально был использован Olaf Kirch в письме в Bugtraq. Мне оно понравилось и оно подходит. Поэтому я его использую. Благодаря Olaf.

Когда "root" != "root", но в тоже время "root" == "root" (уже смущены?)? Когда вы смешиваете языки программирования.

Однажды вечером меня заинтересовало, что же именно может Perl и могу ли я заставить что-то идти не так, как это ожидается. Так я начал передавать очень критичные данные различным системным вызовам и функциям. Ничего захватывающего не произошло кроме одной примечательной вещи...

Как вы видите, я пытался открыть конкретный файл, "rfp.db". Я использовал фальшивый web-сценарий чтобы получить входное значение "rfp" к которому добавляется ".db" и затем открывается файл. В Perl основная часть скрипта выглядит примерно так:

 # parse $user_input $database="$user_input.db"; open(FILE "<$database"); 

Замечательно. Я передаю 'user_input=rfp' и скрипт пытается открыть "rfp.db". Все достаточно просто (давайте пока не будем рассматривать явного упущения /../).

Но интересное началось когда я передал 'user_input=rfp%00'. Perl выполняет $database="rfp\0.db" и затем пытается открыть $database. Последствия? Он открыл "rfp" (или мог бы открыть если бы он существовал). Что же случилось с ".db"? Это интересно.

Видите ли, Perl позволяет нулевые символы в качестве данных содержащихся в переменной. В отличии от C NUL не является конечным символом строки. Но лежащие ниже вызовы системы/ядра написаны на "С". Так что "root" != "root\0". Но вызовы системы/ядра написаны на С, который РАСПОЗНАЮТ NUL как разделитель строки. Что получается в результате? Что Perl передает "rfp\0.db", но лежащие ниже библиотеки останавливаются когда встречают первый NUL.

Что, если у нас есть скрипт, который позволяет отдельным младшим администраторам менять пароли любых пользователей кроме root? Код может выглядеть примерно так:

 $user=$ARGV[1] # user the jr admin wants to change if ($user ne "root"){ # делать все что угодно с этим пользователем} (**ЗАМЕЧАНИЕ: здесь показана упрощенная форма и теория чтобы только проиллюстрировать проблему) 

Так что если младший администратор попробует 'root' в качестве имени, он не сможет что-либо сделать. Но если он передаст 'root\0', то скрипт Perl завершит проверку успешно и выполнит блок. Теперь, когда системный вызов передан (если только не все написано на Perl... что возможно, но невероятно), этот NUL будет успешно потерян и все действия будут происзодить над записью root.

Пока сама по себе эта проблема не является проблемой безопасности, но является достаточно интересной особенностью, за которой можно следить. Я видел множество CGI, которые добавляют ".html" к каким-либо вводимым данным для получения результирующей страницы. Т.е: page.cgi?page=1

Показывает мне 1.html. Это не совсем безопасно, поскольку добавляет ".html", как вы могли бы подумать, в крайнем случае я могу получить только ".html" страницу. Но если мы пошлем page.cgi?page=page.cgi%00 (%00 == '\0' escaped)

То скрипт выдаст нам копию собственных текстов. Даже проверка с помощью опции Perl '-e' не пройдет: $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) откроет его на запись.

Решение? Очень простое! Удалите нули. В Perl это всего лишь

 $insecure_data=~s/\0//g; 

ЗАМЕЧЕНИЕ: не заменяйте их набором метасимволов! Полностью удалите их!

Обратный Слэш


Если вы загляните в FAQ по вопросам безопасности на WWW сервере W3C, то найдете, что рекомендуется следующий список метасимволов:

 &;`'\"|*?~<>^()[]{}$\n\r 

Я же нашел весьма интересным, что все, кажется, забыли про обратный слэш ('\'), может быть это из-за того, как записывается управляющий код на Perl:

 s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g; 

Со всеми этими обратными слешами, которые комментируют [](){} и т.д. забываешь о необходимости убедиться, что и обратный слеш здесь так же перечислен (здесь он '\\'), из-за того, что некоторые люди просто не разбираются в регулярных выражениях и, видя присутствие обратного слеша, думают что он так же перечислен.

Так, в конце концов, почему же это важно? Представьте, что у вас есть следующая строка в CGI:

 user data `rm -rf /` 

И вы прогоняете ее через вашу командную последовательность, которая превращает ее в

 user data \`rm -rf /\` 

которая теперь может быть безопасно выполнена в командах шелл и т.д. Теперь, давайте представим, что вы забыли закомментировать обратный слеш. Пользователь вводит следующую строку:

 user data \`rm -rf / \` 

ваш код превращает ее в:

 user data \\`rm -rf / \\` 

Двойной обратный слеш будет обращен о одиночный обратный слеш в данных, оставляя кавычку не закомментированной. Что приведет к успешному выполнению `rm -rf / \`. Конечно, при таком подходе вам всегда приходится иметь дело с подложным обратным слешем. То, что вы оставите обратный слеш как последний символ в строке вызовет ошибку при обращении Perl к системному вызову или ошибку с кавычкой (по крайней мере в предыдущем примере). Вы должны ускользнуть от этого ;) (это вполне возможно)...

Другой интересный эффект связанный с обратным слешем получается из следующего кода, который запрещает обратный путь в директориях:

 s/\.\.//g; 

Все, что он делает - удаляет двойные точки, эффективно устраняя обращение к файлам более высокого уровня, так что

 /usr/tmp/../../etc/passwd 

превратится в

 /usr/tmp///etc/passwd 

что не сработает (имейте ввиду - повторные слеши разрешены. Попробуйте 'ls -l /etc////passwd')

А теперь введем нашего друга - обратный слеш. Давайте дадим следующую строчку:

 /usr/tmp/.\./.\./etc/passwd 

регулярное выражение не совпадет из-за обратного слеша. А теперь смотрите, как использует такое имя Perl:

 $file="/usr/tmp/.\\./.\\./etc/passwd"; $file=s/\.\.//g; system("ls -l $file"); 

Обратите внимание: в примере использован двойной обратный слеш, чтобы Perl вставил одиночный - иначе Perl будет считать, что вы просто комментируете точку.

С точки зрения данных строка является /usr/tmp/.\./.\./etc/passwd

В тоже время, это работает только на вызове system и вызове с обратной кавычкой. -e в Perl и open (не-конвейерный) не будут работать:

 $file="/usr/tmp/.\\./.\\./etc/passwd"; open(FILE, "<$file") or die("No such file"); 

"умрет" с диагностикой "No such file". По-моему это потому, что требуется шел для преобразования '\.' в '.'.

Решение? Убедитесь, что вы комментируете обратный слеш. Очень просто.

Эта вредная труба


В Perl добавление '|' (конвейер) к концу имени файла в операторе open заставляет Perl выполнить указанный файл а не открыть его. Так

 open(FILE, "/bin/ls") 

выдаст вам кучу двоичного кода, но

 open(FILE, "/bin/ls|") 

на самом деле запустит /bin/ls. Заметьте, что регулярное выражение

 s/(\|)/\\$1/g 

Предотвратит это (Perl "умрет" с диагностикой 'unexpected end of file' поскольку sh требуется следующая строка, которую обозначает '\'. Если вы найдете способ обойти это - пожалуйста дайте мне знать).

Теперь мы можем обобщить ситуацию с другими методами, показанными выше. Давайте предположим, что $FORM это строка, посланная пользователем на вход CGI. Вначале у нас есть:

 open(FILE, "$FORM") 

Так мы можем установить $FORM в "ls|" чтобы получить список директории. Теперь, предположим мы имеем:

 $filename="/safe/dir/to/read/$FORM" open(FILE, $filename) 

тогда нам надо определенным образом указать где находится "ls", так что мы установим $FORM в "../../../../bin/ls|", что даст нам каталог директории. Поскольку это конвейерный open наша техника с обратным слешем может быть использована, если она применима, чтобы обойти анти-обратный-ход в директориях.

До сих пор мы могли использовать опции командной строки в команде. Например, используя приведенный выше кусок кода, мы могли установить $FORM в "touch /myself|" чтобы создать файл /myself

Сейчас у нас более сложная ситуация:

 $filename="/safe/dir/to/read/$FORM" if(!(-e $filename)) die("I don't think so!") open(FILE, $filename) 

Теперь нам придется оставить в дураках '-e'. Проблема в том, что '-e' отвалится, если попробует найти 'ls|' поскольку такого не существует - она просматривает имя файла вместе с настоящим конвейером в конце. Таким образом нам надо убрать конвейер для -e но оставить его для того, чтобы его видел Perl. Что-нибудь приходит на ум? Ядовитый NULL - вот наше спасение! Все что надо сделать - установить $FORM в "ls\0|" (или в форме управляющих символов web "ls%00|". После этого '-e' проверяет наличие 'ls' (он останавливается на NULL игнорируя конвейер). В то же время Perl видит конвейере и отлавливает его и... выполняет нашу команду, он останавливается на NULL - это означает, что мы не можем задать опции командной строки. Может быть этот пример покажет лучше:

 $filename="/bin/ls /etc|" open(FILE, $filename) 

Это дает нам каталог директории /etc

 $filename="/bin/ls /etc\0|" if(!(-e $filename)) exit; open(FILE, $filename) 

Придется так, потому что '-e' увидит, что "/bin/ls /etc" не существует

 $filename="/bin/ls\0 /etc|" if(!(-e $filename)) exit; open(FILE, $filename) 

Это будет работать, но мы получим листинг текущего каталога (как в случае с просто 'ls'), а не листинг директории /etc.

Я хочу также заметить для ущербных программистов: если бы ленивые программисты на Perl (не все, а только ленивые) тратили бы чуть-чуть времени и указывали бы режим доступа к файлу, до речи бы не шло о подобной ошибке.

 $bug="ls|" open(FILE, $bug) open(FILE, "$bug") 

работает, но

 open(FILE, "<$bug") open(FILE, ">$bug") open(FILE, ">>$bug") и т.д. и т.п. 

не работает. Если вы хотите читать файл, то откройте "<$file" а не просто $file. Вставив всего один символ "меньше" вы защитите и себя и свой сервер от кучи неприятностей.

OK, теперь, когда у нас есть оружие, давайте займемся противником.

(беззащитные) скрипты Perl в этой жизни


Наш первый CGU я взял с freecode.com. Это администратор рекламных баннеров. Из файла CGI:

 # First version 1.1 # Dan Bloomquist dan@lakeweb.net 

Теперь первый пример... Дан разбирает все переменные входной формы в %DATA. Он не исключает ни '..' ни NUL. Взглянем на кусочек кода:

 #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' мы можем указать на /etc/passwd. То же самое с $lockfile. Мы не можем использовать конвейер, т.к. он добавляет ".html"/".lock" в конце (вы, конечно, можете попробовать - но работать не будет ;)

 #Read in the classified page open( FILE,"$pageurl" ) || die "can't open to read $pageurl: $!\n"; @lines= <FILE>; close( FILE ); 

Здесь Дан считывает $pageurl, которая является указанным нами файлом. К счастью для Дана, затем он немедленно открывает $pageurl на запись. Так что чтобы мы не указали для чтения - нам нужны так же права на запись. Это ограничивает возможности взлома. Но это служит великолепным примером подобной проблемы.

Достаточно любопытно, что Дан затем продолжает:

 #Send your mail out. # open( MAIL, "|$mailprog $DATA{ 'adEmail' }" ) || die "can't open sendmail: $adEmail: $!\n"; 

Хмммм... здесь ваши обязательные нет-нет.... Дан не разбирает метасимволы shell, так что 'adEmail' становится ужасающим.

Исследуя далее freecode.com, я нашел простую программку записи данных из форм:

 # flexform.cgi # Written by Leif M. Wright # leif@conservatives.net 

Лейф заносит данные в %contents и не комментирует метасимволы shell. Затем он делает следующее:

 $output = $basedir . $contents{'file'}; open(RESULTS, ">>$output"); 

Используя обычный обратный путь в директориях мы можем даже не добавлять NUL. Но опять де нам необходимо везение с разрешениями на файл, который мы хотим открыть. И опять же дырка с конвейером не будет работать, поскольку используется режим добавления к файлу ('>>').

Теперь LWGate, который является WWW-интерфейсом ко многим популярным спискам рассылки.

 # lwgate by David W. Baker, dwb@netspace.org # # Version 1.16 # 

Дэйв помещает разобранные переменные форм в %CGI, затем:

 # 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') 

Хм... кажется Дэйв забыл обратный слэш в регулярном выражении. Ай-яй-яй.

Так. Теперь давайте посмотрим на одно из многих приложений - "тележек для покупок". Опять же с freecode.com, Perlshop.

 $PerlShop_version = 3.1; # A product of ARPAnet Corp. - perlshop@arpanet.com, www.arpanet.com/perlshop 

Интересным является следующее:

 open (MAIL, "|$blat_loc - -t $to -s $subject") || &err_trap("Can't open $blat_loc!\n") 

$to это явно определяемый пользователем email. Blad - почтовая программа NT. Метасимволы в NT это <>&|% (что-то еще?).

Помните про вредную трубу? (надеюсь, что помните... это всего парой параграфов выше!). Признаюсь, это не самая удачная ошибка, но это я ее нашел. Давайте пройдемся далее по архиву скриптов Матта.

 # File Download Version 1.0 # Copyright 1996 Matthew M. Wright mattw@worldwidemart.com 

Сначала он выбирает данные в $Form (ничего не комментируя). Затем выполняет следующее:

 $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; } 

Это вполне удовлетворяет критериям 'проблемы вредной трубы' (tm). Имеется проверка '-e', так что мы не можем использовать аргументы командной строки. Поскольку вначале он приклеивает $BASE_DIR нам придется использовать обратный путь в директории.

Уверен, что читая выше вы встретились с более простой проблемой. Как насчет f=../../../../../../etc/passwd? Да, он существует и может быть прочитан. таким образом вам его должны показать. И покажут. Но с другой стороны: весь доступ к download.cgi записывается в журнал следующим кодом:

 open(LOG,">>$LOG_FILE"); print LOG "$Date|$Form{'s'}|$Form{'c'}|$Form{'f'}\n"; close(LOG); 

Так что, скрытая камера наблюдает за всем, что вы делаете. Но в любом случае - не хорошо причинять зло серверам посторонних людей. ;)

Теперь полетаем вместе с BigNoseBird.com. Я имею ввиду:

 bnbform.cgi #(c)1997 BigNoseBird.Com # Version 2.2 Dec. 26, 1998 

Самое интересное происходит после того, как скрипт открывает конвейер к сендмейлу как MAIL:

 if ($fields{'automessage'} ne "") { open (AM,"< $fields{'automessage'}"); while (<AM>) { chop $_; print MAIL "$_\n"; } 

Еще одна простая вещь. BNB не делает какого-либо разбора входных переменных пользователей ($fields), так что мы можем указать любой файл как 'automessage'. Если он доступен на чтение контексту веб-сервера он будет послан на любой адрес, который мы укажем (по крайней мере теоретически).

А вот и конец


Да, так. К этому времени я слегка устал от разбора программ на Perl. Я оставлю немножко и вам, в качестве домашнего задания. И если вы что-то найдете - черкните мне пару строчек - особенно если вы найдете скрипты, где можно использовать 'проблему вредной трубы'. На этом все. До новых встреч.

Автор: Unknown · Добавлена: 2003-09-16
Просмотров: 2623 · Рейтинг: 5.0

Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]

Категории раздела
Flash
Apache
WWW
PhotoShop
Веб-дизайн
Раскрутка и реклама
Базы данных
3D графика
Хостинг
Истории веб-мастеров
Web-технологии
Сетевая безопасность
Программирование для Web
Операционные системы

Новые статьи
Лучшие статьи
Популярные статьи
Комментируемые статьи
Разделы сайта
Скрипты
Статьи
Шрифты
Флэш исходники
HTML шаблоны
Партнерки
Клипарты
Смайлы
Фоны
Гифы
Иконки
Опрос сайта
Ведете ли вы блог?
Всего ответов: 60122
Наша кнопка
WoWeb.ru - портал для веб-мастера