Введение

В феврале 2024 года группа исследователей уязвимостей Rapid7 выявила уязвимость, затрагивающую сервер JetBrains TeamCity CI/CD и представляющую собой обход аутентификации в веб‑компоненте TeamCity.

Уязвимость получила идентификатор CVE-2024–27 198 и балл CVSS равный 9,8 (критический).

Наличие данного недостатка позволяет полностью скомпрометировать сервер TeamCity, не имея аутентифицированного доступа, включая удаленное исполнение команд.

Компрометация сервера TeamCity позволяет злоумышленнику получить полный контроль над всеми проектами, сборками, агентами и артефактами TeamCity, что может являться подходящим вектором для атаки на цепочку поставок.

Разбор уязвимости

Для начала стоит упомянуть, что TeamCity написан на языке Java с использованием Java Spring framework, который в свою очередь обеспечивает архитектуру паттерна Model — View — Controller (Модель — Отображение — Контроллер) при помощи слабо связанных готовых компонентов. Паттерн MVC разделяет аспекты приложения (логику ввода, бизнес‑логику и логику UI), обеспечивая при этом свободную связь между ними.

  • Model (Модель) инкапсулирует (объединяет) данные приложения.

  • View (Отображение) отвечает за отображение данных Модели, генерируя HTML.

  • Controller (Контроллер) обрабатывает запрос пользователя, создаёт соответствующую Модель и передаёт её для Отображения.

Вся логика работы Spring MVC построена вокруг DispatcherServlet, который получает HTTP‑запросы и отправляет их соответствующим контроллерам. Также важно отметить, что Spring MVC поддерживает множество типов Отображений, в том числе JSP (Java Server Pages) для создания шаблонов.

Теперь перейдем к разбору самой уязвимости.

Основной принцип эксплуатации уязвимости — создание URL-адреса специальным образом, что позволяет обойти аутентификацию и получить доступ к тем эндпоинтам, к которым у неаутентифицированного пользователя доступа нет.

Сама же уязвимость содержится в том, как специальный класс jetbrains.buildServer.controllers.BaseController (имплементированный в библиотеке web-openapi.jar) обрабатывает пользовательские запросы.

Ниже представлена часть кода метода handleRequestInternal из класса BaseController, который осуществляет обработку.

public abstract class BaseController extends AbstractController {
    
    // ...snip...
    
    public final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            ModelAndView modelAndView = this.doHandle(request, response);
            if (modelAndView != null) {
                if (modelAndView.getView() instanceof RedirectView) {
                    modelAndView.getModel().clear();
                } else {
                    this.updateViewIfRequestHasJspParameter(request, modelAndView);
                }
            }
            // ...snip...

В коде данного метода видно, что если результат обработки запроса не перенаправляет пользователя куда-либо (View не является инстансом RedirectView), то будет вызван метод updateViewIfRequestHasJspParameter представленный ниже.

private void updateViewIfRequestHasJspParameter(@NotNull HttpServletRequest request, @NotNull ModelAndView modelAndView) {

    boolean isControllerRequestWithViewName = modelAndView.getViewName() != null && !request.getServletPath().endsWith(".jsp");
        
    String jspFromRequest = this.getJspFromRequest(request);
        
    if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
        modelAndView.setViewName(jspFromRequest);
    }
}

В данном методе мы можем увидеть, что переменная isControllerRequestWithViewName будет иметь значение true, если и у текущего ModelAndView есть имя, и servlet путь текущего запроса не заканчивается на .jsp.

Злоумышленник может удовлетворить это, запросив URI с сервера, который сгенерирует ответ HTTP 404. Такой запрос сгенерирует servlet путь /404.html, что заканчивается на .html, а не .jsp, следовательно isControllerRequestWithViewName будет иметь значение true.

Далее мы видим, как вызывается метод getJspFromRequest, чей результат записывается в переменную jspFromRequest.

Важно отметить, что getJspFromRequest принимает в качестве аргумента изначальный запрос пользователя, что дает злоумышленнику возможность контролировать содержимое переменной jspFromRequest.

Рассмотрим код данного метода.

protected String getJspFromRequest(@NotNull HttpServletRequest request) {
    String jspFromRequest = request.getParameter("jsp");
        
    return jspFromRequest == null || jspFromRequest.endsWith(".jsp") && !jspFromRequest.contains("admin/") ? jspFromRequest : null;
}

В нем из пользовательского запроса берется значение параметра jsp и его содержимое возвращается, если оно заканчивается на .jsp и не содержит в пути сегмент admin/.

Вернемся к методу updateViewIfRequestHasJspParameter.

В конце всех проверок значение переменной jspFromRequest передаётся в метод фреймворка Java Spring ModelAndView.setViewName. И так как злоумышленник может контролировать аргумент этого метода, результат его выполнения позволяет изменить URL-адрес, обрабатываемый DispatcherServlet, что позволяет злоумышленнику вызывать произвольный эндпоинт.

Практическая эксплуатация

Чтобы показать, как использовать эту уязвимость, рассмотрим её на примере эндпоинта /app/rest/server, который возвращает информацию о текущей версии сервера. Если мы напрямую сделаем запрос, он завершится с ошибкой, поскольку мы не прошли аутентификацию.

Чтобы использовать эту уязвимость для успешного вызова /app/rest/server, запрос от неаутентифицированного пользователя должен удовлетворять следующим трем требованиям:

  • Запрос ведет на несуществующий ресурс, вызов которого сгенерирует ответ 404. Например: /giscyberteam

  • Передан параметр HTTP-запроса с именем jsp, содержащий значение аутентифицированного пути URI. Например: ?jsp=/app/rest/server

  • Произвольный путь URI заканчивается на .jsp. Этого можно достичь, добавив сегмент параметра HTTP, например: ;.jsp

Сочетая вышеуказанные требования, зловредный URI-путь может выглядеть следующим образом:

/giscyberteam?jsp=/app/rest/server;.jsp

Используя данный пэйлоад в URI, успешно вызываем нужный эндпоинт.

Злоумышленник может воспользоваться этой уязвимостью с целью обхода аутентификации несколькими способами, один из которых мы продемонстрируем.

Например, неаутентифицированный злоумышленник может создать нового пользователя-администратора со своим паролем используя эндпоинт /app/rest/users:

Результат можно увидеть в web-панели администратора, в разделе с пользователями.

Меры защиты

Компания JetBrains выпустила обновленную версию TeamCity-2023.11.4, в которой устранена данная уязвимость. Разработчики пишут, что проблема затрагивает «все версии до 2023.11.3» и настоятельно рекомендуют установить обновление как можно скорее. Если же это невозможно, для TeamCity 2018.2 и новее, а также для TeamCity 2018.1 и старше уже доступен специальный плагин.

Заключение

В данной статье мы детально разобрали уязвимость в продукте JetBrains «TeamCity», позволяющую обойти аутентификацию и, как следствие, получить полный доступ к серверу.

Подписывайтесь на наш Telegram-канал

Комментарии (0)