
Análisis detallado de CVE-2024-27198: omisión de autenticación en JetBrains TeamCity que conduce a la ejecución remota de código, con explicación a nivel de código y demostración del exploit.
TeamCity es un servidor de integración continua y despliegue que proporciona, de serie, pruebas unitarias continuas, análisis de calidad de código e informes tempranos sobre problemas de compilación.
La vulnerabilidad aparece en una librería que permite a los atacantes acceder a endpoints arbitrarios sin autenticación.
El código vulnerable se encuentra en la librería web-openapi.jar en directory to the lib
La clase jetbrains.buildServer.controllers.BaseController se encarga de manejar las peticiones y respuestas, aunque está implementada de forma incorrecta. Veamos cómo es el código:
public abstract class BaseController extends AbstractController {
//////
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);
}
}
El propósito principal del método ModelAndView es representar la página solicitada en la interfaz de usuario. Aquí es donde comienza el bug. Observa que updateViewIfRequestHasJspParameter se llamará si nuestra petición no está siendo redirigida. Para encontrar la causa raíz de la vulnerabilidad, necesitamos investigar más a fondo para entenderlo.
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);
}
}
Este método se utiliza para actualizar el nombre de la vista del objeto ModelAndView cuando se cumplen ciertas condiciones. isControllerRequestWithViewName será True si el objeto modelAndView tiene un nombre y la path en la URL no termina en .jsp. Por ejemplo, una URL que cumple esas condiciones es http://localhost:8111/random_string y una inválida será http://localhost:8111/admin/admin.html o http://localhost:8111/admin.jsp. Como se discutió anteriormente, no debemos acceder a algo que provoque una redirección; normalmente serán páginas que requieren autorización. El programa asignará entonces una variable llamada jspFromRequest que llamará al método getJspFromRequest(). Pasemos ahora a ese método; explicaré el resto del código después. Hay una sentencia if que comprobará si isControllerRequestWithViewName es verdadero, si el resultado de la llamada al método getJspFromRequest() no está vacío y si el nombre del objeto modelAndView no debe ser igual a la página que queremos solicitar mediante
protected String getJspFromRequest(@NotNull HttpServletRequest request) { String jspFromRequest = request.getParameter("jsp"); return jspFromRequest == null || jspFromRequest.endsWith(".jsp") && !jspFromRequest.contains("admin/") ? jspFromRequest : null; }
Esta función primero recupera el valor de un parámetro de petición llamado jsp. La comprobación asegura que jsp debe terminar en .jsp y no debe contener /admin. Combinando con la sentencia if anterior:
if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
modelAndView.setViewName(jspFromRequest);
}
Tendremos una visión general de cómo se ve la URL:
.jspjsp no debe ser igual a la path. Por ejemplo, /random?jsp=/random será inválidojsp debe terminar en .jspTeamCity proporciona una API REST para integrar aplicaciones externas y crear interacciones mediante scripts con el servidor TeamCity. Permite acceder a los recursos a través de rutas URL. Puedes empezar a trabajar con la API REST abriendo la URL
http://<TeamCity Server host>:<port>/app/rest/serveren tu navegador: esta página ofrece varios enlaces para explorar la API.
Teamcity ofrece una API REST que nos permite acceder a recursos sensibles si podemos evadir la autenticación. En este caso, por ejemplo, intentaremos acceder a /app/rest/server.

Como es habitual, el servidor nos redirigirá a /login.html cuando solicitemos /app/rest/server. Construyamos una URL perfecta para evadir esta restricción. Primero, nuestra ruta puede ser cualquier cosa siempre que devuelva un código de estado 404 o incluso 200, como login.html. A continuación, usaremos jsp para solicitar /app/rest/server, pero debe terminar en .jsp. En esta situación, incluso hay 2 trucos que podemos usar para evadir esta comprobación. Podemos usar el punto y coma ; como delimitador de parámetros: /app/rest/server;.jsp; esta vez, .jsp se tratará como un segundo parámetro. La segunda evasión es usar el fragmento de URI #: /app/rest/server%23.jsp. Lo que va después del fragmento de URI no se usará para el enrutamiento, solo se usa para la navegación dentro de una página. Ten en cuenta que el carácter debe estar codificado en URL; de lo contrario, el navegador lo ignorará primero.

Con esta técnica, incluso podemos crear un nuevo usuario con privilegios de administrador. La documentación de Teamcity dice que podemos crear un nuevo usuario mediante este endpoint /app/rest/users.

Vayamos al panel de administración y comprobemos si se ha creado algún usuario nuevo.

Como era de esperar, se crea un nuevo usuario con privilegios de administrador. Con los privilegios de administrador, podemos controlar completamente el servidor. Sin embargo, podemos ir aún más lejos obteniendo ejecución remota de código. Este CVE afecta a todas las versiones anteriores a 2023.11.4, pero este RCE solo es posible en versiones anteriores a 2023.11. Hay un endpoint no documentado /app/rest/debug/processes que permite a un usuario con privilegios de administrador ejecutar comandos arbitrarios. Enviaremos una petición POST con 2 parámetros de petición en la URL de la solicitud. Para Windows, será ?exePath=cmd.exe¶ms=/c%20[our command here] y con Linux será ?exePath=/bin/sh¶ms=-c%20[our command here]. Estoy ejecutando Teamcity en Windows, por lo que la ruta completa será /app/rest/debug/processes?exePath=cmd.exe¶ms=/c%20whoami

El servidor devuelve un error 403 indicando que la petición carece de csrf token. La documentación de Teamcity también nos proporciona el endpoint para recuperar el token, que es /authenticationTest.html?csrf.
Después de recuperar el token, lo añadiremos a la cabecera de la petición X-TC-CSRF-Token o al parámetro HTTP tc-csrf-token; aquí elijo X-TC-CSRF-Token.