Пользуясь случаем, рассмотрим применение кейса на реальном примере, который был раскручен с его помощью до RFI (Remote File Include).
Для локального воспроизведения нам понадобится
WordPress 5.5, а так же плагин
WP-Live Chat by 3CX версии 9.0.17, который на текущий момент имеет 50 тысяч активных установок.
Запускать всё это дело будем на стенде с
Debian 9, веб-сервером
Apache/2.4.25 и
PHP 7.3.15 с включенным выводом ошибок (
display_errors = On).
После нехитрой установки WP и плагина, вычищаем все админские куки и следуем на главную страницу нашего новоиспечённого блога.
Следующим шагом, проверяем теорию описанную в первом посте, при помощи простейшего теста, с подставлением GET параметра
"?this=1".
Получаем фатальную ошибку, с раскрытием локальных путей, а так же номером строки где она была вызвана. Далее открываем файл в любимом текстовом редакторе и находим это место. Наш кейс всплывает в функии
evaluate_php_template:
./wp-live-chat-support/includes/helpers/utils_helper.php:
PHP код:
PHP:
[COLOR="#000000"][COLOR="#0000BB"][/COLOR][COLOR="#007700"]. . .
public static function[/COLOR][COLOR="#0000BB"]evaluate_php_template[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$path[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$args[/COLOR][COLOR="#007700"]) {
foreach ([/COLOR][COLOR="#0000BB"]$args[/COLOR][COLOR="#007700"]as[/COLOR][COLOR="#0000BB"]$key[/COLOR][COLOR="#007700"]=>[/COLOR][COLOR="#0000BB"]$value[/COLOR][COLOR="#007700"]) {
${[/COLOR][COLOR="#0000BB"]$key[/COLOR][COLOR="#007700"]} =[/COLOR][COLOR="#0000BB"]$value[/COLOR][COLOR="#007700"];
}
[/COLOR][COLOR="#0000BB"]ob_start[/COLOR][COLOR="#007700"]();
include([/COLOR][COLOR="#0000BB"]$path[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$var[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]ob_get_contents[/COLOR][COLOR="#007700"]();
[/COLOR][COLOR="#0000BB"]ob_end_clean[/COLOR][COLOR="#007700"]();
return[/COLOR][COLOR="#0000BB"]$var[/COLOR][COLOR="#007700"];
}
. . .
[/COLOR][/COLOR]
, где невооружённым глазом видно теоретическую возможность удалённого инклуда.
Если скрипт упал в этом месте, значит пользовательский ввод попадает в
$args. Для того чтобы убедиться в этом, поищем в коде плагина места где используется эта функия и в свою очередь обнаружим другую функцию -
load_view:
./wp-live-chat-support/includes/wplc_base_controller.php:
PHP код:
PHP:
[COLOR="#000000"][COLOR="#0000BB"][/COLOR][COLOR="#007700"]. . .
protected function[/COLOR][COLOR="#0000BB"]load_view[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$filepath[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$return_html[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]false[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$add_wrapper[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]true[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$children[/COLOR][COLOR="#007700"]= array() ) {
[/COLOR][COLOR="#0000BB"]$data[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]convert_view_data[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]view_data[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$data[/COLOR][COLOR="#007700"][[/COLOR][COLOR="#DD0000"]"page_title"[/COLOR][COLOR="#007700"]] =[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]page_title[/COLOR][COLOR="#007700"];
[/COLOR][COLOR="#0000BB"]$view_data[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]array_merge[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$data[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$_GET[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$view_data[/COLOR][COLOR="#007700"][[/COLOR][COLOR="#DD0000"]'wplc_settings'[/COLOR][COLOR="#007700"]] =[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]wplc_settings[/COLOR][COLOR="#007700"];
[/COLOR][COLOR="#0000BB"]$view_data[/COLOR][COLOR="#007700"][[/COLOR][COLOR="#DD0000"]'selected_action'[/COLOR][COLOR="#007700"]] =[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]selected_action[/COLOR][COLOR="#007700"];
unset([/COLOR][COLOR="#0000BB"]$data[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$data_literal[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]generate_wrapper_data[/COLOR][COLOR="#007700"]();
[/COLOR][COLOR="#0000BB"]$view_html[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]TCXUtilsHelper[/COLOR][COLOR="#007700"]::[/COLOR][COLOR="#0000BB"]evaluate_php_template[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$filepath[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$view_data[/COLOR][COLOR="#007700"]);
if([/COLOR][COLOR="#0000BB"]$add_wrapper[/COLOR][COLOR="#007700"]) {
[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#DD0000"]''[/COLOR][COLOR="#007700"];
[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"].=[/COLOR][COLOR="#0000BB"]$view_html[/COLOR][COLOR="#007700"];
[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"].=[/COLOR][COLOR="#DD0000"]''[/COLOR][COLOR="#007700"];
}else
{
[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$view_html[/COLOR][COLOR="#007700"];
}
if ([/COLOR][COLOR="#0000BB"]count[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$children[/COLOR][COLOR="#007700"]) >[/COLOR][COLOR="#0000BB"]0[/COLOR][COLOR="#007700"]) {
[/COLOR][COLOR="#0000BB"]libxml_use_internal_errors[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]true[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"]= new[/COLOR][COLOR="#0000BB"]DOMDocument[/COLOR][COLOR="#007700"]();
[/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]formatOutput[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]true[/COLOR][COLOR="#007700"];
[/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]loadHTML[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"]);
foreach ([/COLOR][COLOR="#0000BB"]$children[/COLOR][COLOR="#007700"]as[/COLOR][COLOR="#0000BB"]$child[/COLOR][COLOR="#007700"]) {
[/COLOR][COLOR="#0000BB"]$container_element[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]getElementById[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$child[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]id[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$html[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$child[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]controller[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]view[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]true[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]false[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$node[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$this[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]createElementFromHTML[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"],[/COLOR][COLOR="#0000BB"]$html[/COLOR][COLOR="#007700"]);
[/COLOR][COLOR="#0000BB"]$container_element[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]appendChild[/COLOR][COLOR="#007700"]([/COLOR][COLOR="#0000BB"]$node[/COLOR][COLOR="#007700"]);
}
[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"]=[/COLOR][COLOR="#0000BB"]$doc[/COLOR][COLOR="#007700"]->[/COLOR][COLOR="#0000BB"]saveHTML[/COLOR][COLOR="#007700"]();
}
if ([/COLOR][COLOR="#0000BB"]$return_html[/COLOR][COLOR="#007700"]) {
return[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"];
} else {
echo[/COLOR][COLOR="#0000BB"]$result_view[/COLOR][COLOR="#007700"];
return[/COLOR][COLOR="#0000BB"]true[/COLOR][COLOR="#007700"];
}
}
. . .
[/COLOR][/COLOR]
На третьей строке этой функции видим, что переменная
$data объединяется с массивом
$_GET. Это как раз то место, когда пользовательские данные без какой-либо обработки попадают в уязвимую функцию
evaluate_php_template. А это значит, мы можем изменить переменную
$path на любое значение. И сделав запрос вида
http://wordpress/?path=/etc/passwd, убеждаемся в этом:
Или так:
Теперь давайте разбираться как так произошло
На первом скрине видим, что
load_view() в плагине используется повсеместно, для отображения различных страниц темплейтов, с предустановленными зарание переменными и юзеринпутом. Важным аргументом для неё является
$filepath, который, как видно, при вызове жёстко прописывается и повлиять на него никак нельзя.
Далее, подготавливаются некие переменные, для подключаемого шаблона, и массив с этими переменными объединяется с $_GET. И через несколько строк,
$view_data, с пользовательскими гет данными, отправляется в функцию
evaluate_php_template(), куда так же первым аргументом передаётся захардкоженный
$filepath.
В следующем методе аргументы
$args, возможностями
переменных переменных глобализуются или переназначаются (
неявно) в контексте нашей функции, вероятно, для заполнения подключаемого темплейта.
Всё бы ничего, но мы передав
?path=/etc/passwd сделали так, что один из ключей перебираемого массива
$args является
"path", что переназначит значение, казалось бы жёстко прописанной, переменной
$path переданой заранее!
$this is the end