Информационная безопасность
[RU] switch to English


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



-------[  Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 07 of 19  ]


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


--------[  rain.forest.puppy / [ADM/Wiretrip]   ]


----------------[  Введение

Мне кажется, что я должен немного пояснить о чем будет идти речь. По большей 
части я сам писал и просматривал различные 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 [email protected]

Теперь первый пример... Дан разбирает все переменные входной формы в %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= ;
        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
        # [email protected]


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

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

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

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

        # lwgate by David W. Baker, [email protected] # 
        # 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. - 
                [email protected], 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  [email protected]

Сначала он выбирает данные в $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 () {
                        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 ()
             {
              chop $_;
              print MAIL "$_\n";
             }

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


----------------[ А вот и конец

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

.rain.forest.puppy. [ADM/Wiretrip]  [email protected]

Greets can be found at http://www.el8.org/~rfp/greets.html

----[  EOF
О сайте | Условия использования
© SecurityVulns, 3APA3A, Владимир Дубровин
Нижний Новгород