По мотивам https://krober.biz/?p=3306 (веб-архив), с печальным выводом: До этого момента, с таким контекстом не встречался и в голову так же не приходило. Поэтому, после прочтения заметки, один юз кейс сразу нарисовался и спешу поделиться с вами Некоторые PHP разработчики, за каким-то хреном, используют эмуляцию почившего режима Register Globals (RG). Выглядеть это может по разному: PHP: <?phpextract($_GET); или PHP: <?phpparse_str($_SERVER['QUERY_STRING']); или PHP: <?phpforeach($_GET as $k => $v) $$k = $v;unset($GET); и пр. Понятное дело, что при вайтбоксе оно нам и даром не нужно, у нас же всё как на ладони. Но вот при блекбоксе пых приложух, фишечка может очень сильно помочь задетектить такой вот своеобразный RG. Соответственно, тут всплывают все прелести атак типа: Code: http://site.com/upload.php?_FILES[file][name]=image.jpg&_FILES[file][type]=image/jpeg&_FILES[file][tmp_name]=/etc/passwd&_FILES[file][error]=0&_FILES[file][size]=1000 или Code: httр://site.com/upload.php?_SERVER[DOCUMENT_ROOT]=zip:///var/lib/php/sessions/sess_test%23 или Code: httр://site.com/upload.php?_SESSION[admin]=1 и другие варианты, в зависимости от предполагаемой логики работы чёрного ящика Парочка примеров:
Для тех, кто любит автоматизацию, плагин для Nessus, который определяет наличие такой ситуации по наличию в заголовках или теле ответа характерного текста Code: # Pseudo register globals detection include("compat.inc"); if(description) { script_id(10797109); script_version("1.0"); script_cvs_date("$Date: 2019/12/19 13:37:00 $"); script_name(english: "Pseudo Register Globals Detection"); script_set_attribute(attribute: "synopsis", value: "Possible pseudo register globals behavior was detected on the remote host."); script_set_attribute(attribute: "description", value: "Possible pseudo register globals behavior was detected on the remote host."); script_set_attribute(attribute: "solution", value: "Check existing source code. Consider rewriting source code without usage of constructions like: extract($_GET), parse_str($_SERVER['QUERY_STRING']) , etc..."); script_set_attribute(attribute: "see_also", value: "https://antichat.com/threads/474727/"); script_set_attribute(attribute: "risk_factor", value: "Low"); script_set_attribute(attribute: "plugin_publication_date", value: "2019/12/19"); script_set_attribute(attribute: "plugin_type", value: "remote"); script_end_attributes(); script_summary(english: "Reports if response with code 500 occurs upon sending '/?this=abc' request. Additional checks should be made manually."); script_category(ACT_GATHER_INFO); script_copyright(english: "This script is Copyright (C) Kaimi (https://kaimi.io)"); script_family(english: "CGI abuses"); script_dependencie("webmirror.nasl", "DDI_Directory_Scanner.nasl"); script_exclude_keys("Settings/disable_cgi_scanning"); script_require_keys("Settings/enable_web_app_tests"); script_require_ports("Services/www"); script_timeout(1800); exit(0); } include("audit.inc"); include("global_settings.inc"); include("misc_func.inc"); include("http.inc"); app = "PHP"; port = get_http_port(default: 80); dirs = list_uniq(make_list(cgi_dirs(), get_kb_list("www/" + port + "/content/directories"), "")); found_list = make_list(); found_ctr = 0; foreach dir (dirs) { path = dir + '/?this=abc'; res = http_send_recv3( method : "GET", port : port, item : path ); if(isnull(res)) continue; if ( # Check headers first string eregmatch(pattern: '500 Internal Server Error', string: res[0], icase: TRUE) || # Check body eregmatch(pattern: 'Internal Server Error', string: res[2], icase: TRUE) ) { found_list[found_ctr] = path; found_ctr++; } } if(found_ctr > 0) { report = NULL; if (report_verbosity > 0) { report += '\nNessus was able to detect a suspicious behavior by the following paths:\n'; report += '\n'; for (i = 0; i < found_ctr; i++) { url = found_list[i]; report += 'URL\t\t: ' + build_url(port: port, qs: url) + '\n'; report += '\n'; } } security_note(port: port, extra: report); } else { audit(AUDIT_WEB_APP_NOT_AFFECTED, app, build_url(port: port, qs: "/")); }
Пользуясь случаем, рассмотрим применение кейса на реальном примере, который был раскручен с его помощью до 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: . . . public static function evaluate_php_template( $path, $args ) { foreach ( $args as $key => $value ) { ${$key} = $value; } ob_start(); include( $path ); $var = ob_get_contents(); ob_end_clean(); return $var; }. . . , где невооружённым глазом видно теоретическую возможность удалённого инклуда. Если скрипт упал в этом месте, значит пользовательский ввод попадает в $args. Для того чтобы убедиться в этом, поищем в коде плагина места где используется эта функия и в свою очередь обнаружим другую функцию - load_view: ./wp-live-chat-support/includes/wplc_base_controller.php: PHP: . . . protected function load_view( $filepath, $return_html=false, $add_wrapper=true, $children = array() ) { $data = $this->convert_view_data( $this->view_data ); $data["page_title"] = $this->page_title; $view_data = array_merge( $data, $_GET ); $view_data['wplc_settings'] = $this->wplc_settings; $view_data['selected_action'] = $this->selected_action; unset( $data ); $data_literal = $this->generate_wrapper_data(); $view_html =TCXUtilsHelper::evaluate_php_template( $filepath, $view_data ); if($add_wrapper) { $result_view = '<div id="wplc_wrapper" ' . $data_literal . '>'; $result_view .= $view_html; $result_view .= '</div>'; }else { $result_view = $view_html; } if ( count( $children ) > 0 ) { libxml_use_internal_errors( true ); $doc = new DOMDocument(); $doc->formatOutput = true; $doc->loadHTML( $result_view ); foreach ( $children as $child ) { $container_element = $doc->getElementById( $child->id ); $html = $child->controller->view(true,false); $node = $this->createElementFromHTML( $doc,$html ); $container_element->appendChild( $node ); } $result_view = $doc->saveHTML(); } if ( $return_html ) { return $result_view; } else { echo $result_view; return true; } }. . . На третьей строке этой функции видим, что переменная $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