Дата: 07.07.2010 | Владимир D0znpp Воронцов | Сайт: onsec.ru/ | Хакер №4/10
Сегодня я хочу рассказать тебе о некоторых особенностях функционирования веб-приложений, которые могут повлиять на их безопасность. Прежде всего, предлагаю обратить внимание на различия между терминами "безопасность сайта" и "безопасность системы управления сайтом". В самом деле, хотя эти вещи и взаимосвязаны, но, как показывает практика – они всего лишь пересекающиеся множества. Пентестер, выполняющий аудит конкретного сайта методом "черного ящика" может выявить недостатки CMS, на которой этот сайт работает, а может и не выявить. Как повезет.
Никогда не сдавайся
Немного лирики… Однажды мне довелось исследовать одну очень хорошо защищенную систему. Мозговые штурмы следовали один за другим и ничего не приносили, идеи иссякали, а результат оставался практически нулевым. Я начал писать разнообразные фаззеры, дергая то один, то другой скрипт в надежде вытащить хоть что-нибудь, но все было без толку. Однако, крылатая фраза на спичечном коробке с цаплей и лягушкой, покорившем сердца многих наших соотечественников, оказалась как всегда безукоризненно верной. В куче ответов сервера на разнообразные запросы в глаза бросились ответы аномально маленькой длины. После их более подробного изучения стало ясно, что сервер периодически не успевал отрабатывать мои навороченные запросы за max_execution_time и скрипт падал с 500-м статусом. Это было уже что-то, так как в ошибке содержались абсолютные пути и имена скриптов на сервере. Выудив самый тяжелый для сервера запрос (им оказалась функция создания миниатюры из формата TIFF), я поставил его в цикл в многопоточном режиме и стал собирать информацию. Через непродолжительное время у меня были ответы 11 различных типов, каждый из которых раскрывал имя и путь к своему классу. Второй раз счастье улыбнулось в гугле, когда оказалось, что один из этих классов доступен для скачивания на просторах Сети. После изучения исходника были выявлены слабые места и проведена атака переопределения переменной с Register_Globals=ON. Подбирать имя этой переменной, не видя исходников, можно было годами… Движок сдался, а полезный опыт и побудил меня к написанию этой статьи.
Настройки PHP
После такого дебюта сразу стало интересно найти другие возможные пути к проведению схожих атак. В настройках интерпретатора PHP были выделены следующие опции:
max_execution_time
max_input_nesting_level
max_input_time
memory_limit
pcre.backtrack_limit (PHP>=5.2.0)
pcre.recursion_limit (PHP>=5.2.0)
post_max_size (PHP>=4.0.3)
upload_max_filesize
max_file_uploads (PHP>=5.2.12)
Здесь не все, но наиболее распространенные опции, что называется common . Весь список опций (включая различные модули) можно найти на php.net/manual/en/ini.list.php. Искать по ключевым словам max, limit. Из всех параметров следовало выявить наиболее применимые. Тут я руководствовался, прежде всего, универсальностью: хотелось найти параметры, которые удастся компрометировать на как можно более широком спектре настроек PHP и веб-серверов. После долгих мучений, описывать которые здесь не буду, оказалось, что самые пригодные к использованию – max_execution_time, memory_limit. Они выбрасываются в тело ответа при настройках error_reporting=E_ERROR или выше, и display_errors=On.
Такое можно встретить в большинстве дефолтных конфигов. Кроме того, варьируя значения переменных, можно добиться выпадания ошибок из различных мест приложения. Врезультате мы получим не только названия классов, скриптов, пути к приложению, но и понятие об иерархии вызовов внутри приложения. Но это еще не все данные, которые нужно иметь для начала работы.
Подготовительный этап – URI max length и max_input_nesting_level
Для начала напишем простые скрипты для определения двух параметров сервера – максимальной длины GET-запроса и максимальной глубины вложенности входных данных. Зачем они пригодятся будет рассказано дальше. Максимальная длина запроса устанавливается веб-сервером, определить ее очень просто методом дихотомии (деления отрезка пополам). Код на PHP выглядит примерно так:
function fuzz_max_uri_len($url){
$headers = array();
$data = array();
$left = 500;//Значение левого края искомого диапазона
$right = 64000;//Значение правого края искомого диапазона
$accur = 5;//Точность, с которой определяем значение
while (($right-$left) > $accur){
$cur = ($right+$left)/2;
$data['x'] = str_repeat("x",$cur);
list($h,$c,$t) = sendGetRequest($url, $headers, $data);
$s = intval(substr($h,9,3));
if ($s<400){
$left=$cur;
}
else{
$right=$cur;
}
echo "\n$cur\t$s";
}
return(($right+$left)/2);
}
Второй необходимый параметр max_input_nesting_level – свойство уже строго настройки интерпретатора, по умолчанию равен 64. Это значение определяет максимальную размерность массива, которую может иметь переменная, приходящая от пользователя. Рассмотрим для примера вот такой код:
<?php echo $_GET[‘a’]; ?>
В случае, если, max_input_nesting_level=1 и мы передадим в запросе ?a[][], на экране ничего не появится, в интерпретаторе возникнет ошибка уровня Notice, говорящая о том, что переменная не объявлена. Если же мы увеличим значение параметра до 2 и повторим запрос, на экране уже высветится "Array". Казалось бы, именно таков самый простой способ определить значение этого параметра – найти скрипт, который в явном виде выводит значение какой-нибудь пользовательской переменной и вызывать его, увеличивая вложенность, пока не исчезнет надпись Array. Такой поиск опять-таки стоит проводить методом дихотомии. Однако я попытался написать более универсальный алгоритм, который будет работать даже в случае, когда в ответ выводятся переменные, только косвенно зависящие от пользовательской. До сих пор не уверен по поводу оптимальности выбранного алгоритма, так что представляю его на суд общественности . Суть в том, чтобы постепенно увеличивать значение размерности массива и анализировать количество байт ответа. Если длина ответа отличается от предыдущего больше чем на какое-то пороговое значение, это считается аномалией и фиксируется в логе. Дополнив мой PoC нехитрой функцией построения графиков, я получил интересные картинки, которые представлены в сносках. В большинстве случаев, по спаду графика зависимости размера ответа от размерности массива и определяется значение параметра. Этот алгоритм пригодился мне и в дальнейшем, плюс я написал аналогичный статистический анализатор для времени ответа сервера.
Чрезмерное употребление памяти вредит вашему скрипту
Вернемся к нашей святой цели – спровоцировать ошибку "Allowed memory size exhausted". В качестве самого тривиального примера, рассмотрим PHP-код <?php echo ‘OK’;?>.
Казалось бы, какое тут потребление памяти?! На самом деле, такой скрипт может жрать мегабайты ОЗУ. И тут, не спорю, нет вины программиста, написавшего его. Для вывода размера используемой памяти в PHP служит функция memory_get_usage(). Предлагаю дописать ее к тривиальному скрипту и провести некоторые измерения. Для начала вызовем наш скрипт не с переменной, a методом GET. Потребление возрастет где-то на 1 Кб. Интерпретатор уже выделил немного памяти под значение переменной, поэтому, если послать запрос "?a=aaa", потребление памяти не увеличится. Наша же задача – получить как можно больше выделенной памяти при как можно более короткой длине GET-запроса (максимальное значение мы уже получили и держим в уме). Попробуем теперь передать запрос с параметром ?a[], количество потребленной памяти увеличится уже примерно на 500 байт. Теперь в игру вступает второй параметр, который был определен выше – max_input_nesting_level. Как только размерность нашего массива превысит его, потребление памяти будет равносильно случаю, когда мы вообще ничего не передаем. Для эксперимента я проверил, сколько же памяти будет потреблять тривиальный скрипт если нет ограничения на размерность массива. Оказалось, что при запросе ?a([]x2500 раз) тривиальный скрипт ест около 1.2 Мб. Этого, конечно, слишком мало, чтобы вывалиться за memory_limit, но и скрипт наш не похож на реальное веб-приложение. Чтобы мониторить потребление памяти любого приложения, можно написать очень простой скрипт:
<?php echo “marker:”.memory_get_usage().”#”; ?>
и добавить его в директиву auto_append_file в php.ini. Теперь нетрудно написать функцию, которая будет искать в ответе сервера маркер и получать значение потребляемой памяти. Функция будет такая:
function findMarker($content){
$p1 = strpos($content, "ONsec E500 mem:");
if ($p1===false){
return 0;
}
else{
$p2=strpos($content,"#",$p1);
if ($p2===false){
return 0;
}
else{
$mem=substr($content,$p1+15,$p2-$p1-15);
}
}
return intval($mem);
}
Теперь мы можем попытаться получить практическую пользу от всего написанного. Тут следует запастись удачей. Навскидку, без исходного кода определить скрипты, которые любят память, может быть непросто. Совет такой – ищи циклы с обработкой массивов, рекурсии и всего такого же плана. В ряде случаев может оказаться, что лучшеиспользовать POST, где существенно больше ограничения на длину передаваемых данных. Советую взять с диска мой PoC и посмотреть функцию fuzz_memory_usage(). Ее можно использовать для перебора переменных различными методами (POST,GET,Multipart) и для выявления наиболее выгодных для выделения памяти комбинаций. Там же встроена проверка на аномальныю длину и время ответа, так что, если долгожданная ошибка появится, ты ее не пропустишь.
Медленный скрипт – уязвимый скрипт
В отличие от потребления памяти, время выполнения скрипта зависит от нагрузки на сервер и вообще является величиной, мягко говоря, непостоянной. Заставить приложение отрабатываться дольше, чем указано в параметре max_execution_time, непросто. Есть даже класс уязвимостей в OWASP, называется "dead_code". Это ошибки разработчика, которые можно эксплуатировать в целях взлома, например, для провоцирования ошибки превышения времени выполнения. Тестируя приложение или сайт, ты уже имеешь какое-то представление о том, какие запросы отрабатывают быстрее, а какие медленнее, чем другие. Это, опять же, всевозможные циклы. Кстати, фильтры безопасности часто грешат медленной скоростью выполнения. Особенно это касается фильтров, исправляющих запрос. Зная как работает фильтр, можно скормить ему запрос, для приведения которого потребуется много итераций.
Кроме того, опасны операции с файлами, например, злоумышленник может попробовать загрузить большой файл в несколько потоков. Если веб-приложение попытается записать файл в то же место, куда еще не дописался этот же файл от другого запроса, то оно несколько "промедлит". Но, опять же, все зависит от используемых функций, ОС, ФС, настроек и многих факторов. Вот общие рекомендации, которые можно дать для поиска уязвимых скриптов. В общем случае, постоянно увеличивая нагрузку на сервер, злоумышленник рано или поздно все равно получит то, на что рассчитывает. Конечно, и такие старания нетрудно пресечь, но это уже выходит за рамки веб-приложения.
Рассмотрим теперь живой пример на последней версии Битрикса и тестовой площадке. В системе были выявлены некоторые особенности, а именно:
При загрузке файла в качестве аватара, он помещается в директорию с трехсимвольным именем, диапазон символов хексовый (16^3=4096).
При обновлении аватара, директория со старым аватаром удаляется.
При загрузке аватара с именем длиннее 250 символов, директория создается, а файл не загружается. Созданная таким образом директория уже не удаляется.
Можно рассчитывать на то, что обильное количество созданных директорий будет увеличивать время выполнения скрипта загрузки аватара. Проверить это можно простым запросом Multipart, запущенным в несколько потоков. Опять-таки, проверяем на аномалии по длине и времени ответа, сохраняя такие результаты в файлы. Запустив такой алгоритм в 20 потоков, я получил файлы, отличающиеся по длине.
Разбираем результаты
По завершении отлова заветных ответов дело остается за малым – аккуратно разобрать их, вычленить пути из сообщений об ошибках и расположить по уровням в зависимости от длины ответа. Это решается примерно таким кодом:
function parseResults($dir){
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
$i=0;
$results = array();
while (($file = readdir($dh)) !== false) {
$curFile = $dir.$file;
$fh = fopen($curFile, 'r');
$filedata = fread($fh, filesize($curFile));
$fsize = filesize($curFile);
$p1 = strpos($filedata,"Maximum execution time of ");
if ($p1 === false) {}
else{
$p2 = $p1+52;
$p3 = strpos($filedata,"</b>",$p2);
if ($p3 === false) {}
else{
$len = $p3-$p2;
$path = substr($filedata,$p2,$len);
$unique = true;
//Проверяем на уникальность
foreach($results as $key=>$value){
if ($value['path']==$path){
$unique=false;
break;
}
}
if ($unique){
$len = $p3-$p2;
$res = array( 'path'=>substr($filedata,$p2,$len),'len'=>$fsize);
$results[$i]=$res;
$i++;
}
}
}
fclose($fh);
}
closedir($dh);
$size=count($results)-1;
//Сортируем результаты по длине
for ($i = $size; $i>=0; $i--) {
for ($j = 0; $j<=($i-1); $j++)
if ($results[$j]['len']>$results[$j+1]['len']) {
$k = $results[$j];
$results[$j] = $results[$j+1];
$results[$j+1] = $k;
}
}
return $results;
}
}
}
На выходе получаем отсортированный массив с длинами ответов и именами скриптов, в которых возникла ошибка. Самое приятное – можно восстановить хоть весь стек, только это займет значительное время. К слову, на своей виртуалке я наловил 126 классов за 30 минут. Остается оформить отчет по уровням иерархии в красивом формате. Собственно, все это внутри PoC и содержится – пользуйся на здоровье!
Заключение
Это конечно не все возможные варианты получения информации через провокацию ошибок. Существует еще множество вариантов, методик и техник, применимых как для конкретных сайтов, так и для движков целиком. Все эти техники, приемы и методы предстоит еще найти и использовать, публиковать и модернизировать. Есть и множество проблем – например, оптимизировать PoC для уменьшения количества запросов и уменьшения следов в логах. Эта статья преследовала цель показа основ техники. Надеюсь, получилось. Как всегда, отвечаю в блоге на вопросы.
WWW
oxod.ru – мой блог, отвечаю на вопросы, пишу по мере сил.
php.net – официальный сайт интерпретатора, сюда за параметрами конфига.