
Analyse détaillée de CVE-2024-27198 : contournement de l'authentification dans JetBrains TeamCity conduisant à une exécution de code à distance, avec explication au niveau du code et démonstration d'exploitation.
TeamCity est un serveur d'intégration continue et de déploiement qui fournit, par défaut, des tests unitaires continus, une analyse de qualité de code et un reporting précoce sur les problèmes de build
La vulnérabilité se trouve dans une bibliothèque qui permet aux attaquants d'accéder à des points de terminaison non authentifiés arbitraires.
Le code vulnérable se trouve dans la bibliothèque web-openapi.jar dans directory to the lib
La classe jetbrains.buildServer.controllers.BaseController est en charge de la gestion des requêtes et des réponses, mais implémentée de manière incorrecte. Voyons à quoi ressemble le code :
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);
}
}
Le but principal de la méthode ModelAndView est de rendre la page demandée à l'interface utilisateur. C'est là que le bogue commence. Notez que updateViewIfRequestHasJspParameter sera appelée si notre requête n'est pas redirigée. Pour trouver la cause racine de la vulnérabilité, nous devons approfondir l'investigation.
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);
}
}
Cette méthode est utilisée pour mettre à jour le nom de vue de l'objet ModelAndView lorsqu'il répond à des conditions particulières. isControllerRequestWithViewName sera True si l'objet modelAndView a un nom et que le path dans l'URL ne se termine pas par .jsp. Par exemple, une URL qui satisfait ces conditions est http://localhost:8111/random_string et une invalide sera http://localhost:8111/admin/admin.html ou http://localhost:8111/admin.jsp. Comme discuté ci-dessus, nous ne devons pas accéder à quelque chose qui déclenche une redirection, généralement des pages qui nécessitent une autorisation. Le programme assigne ensuite une variable nommée jspFromRequest qui appellera la méthode getJspFromRequest(). Passons maintenant à cette méthode, j'expliquerai le code restant après. Il y a une instruction if qui vérifiera si isControllerRequestWithViewName est vrai, le résultat de l'appel à la méthode getJspFromRequest() n'est pas vide et le nom de l'objet modelAndView ne doit pas être égal à la page que nous voulons demander via
protected String getJspFromRequest(@NotNull HttpServletRequest request) { String jspFromRequest = request.getParameter("jsp"); return jspFromRequest == null || jspFromRequest.endsWith(".jsp") && !jspFromRequest.contains("admin/") ? jspFromRequest : null; }
Cette fonction récupère d'abord la valeur d'un paramètre de requête appelé jsp. La vérification garantit que jsp doit se terminer par .jsp et ne doit pas contenir /admin. En combinant avec l'instruction if ci-dessus :
if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
modelAndView.setViewName(jspFromRequest);
}
Nous aurons une vue d'ensemble de ce à quoi ressemble l'URL :
.jspjsp ne doit pas être égal au path. Par exemple, /random?jsp=/random sera invalidejsp doit se terminer par .jspTeamCity fournit une API REST pour intégrer des applications externes et créer des interactions scriptées avec le serveur TeamCity. Elle permet d'accéder aux ressources via des chemins d'URL. Vous pouvez commencer à travailler avec l'API REST en ouvrant l'URL
http://<TeamCity Server host>:<port>/app/rest/serverdans votre navigateur : cette page donne plusieurs pointeurs pour explorer l'API.
TeamCity offre une API REST qui nous permet d'accéder à des ressources sensibles si nous pouvons contourner l'authentification. Dans ce cas, par exemple, nous allons essayer d'accéder à /app/rest/server.

Comme d'habitude, le serveur nous redirige vers /login.html lorsque nous demandons /app/rest/server. Construisons une URL parfaite pour contourner cette restriction. Tout d'abord, notre chemin peut être n'importe quoi tant qu'il retourne un code d'état 404 ou même 200 comme login.html. Ensuite, nous utiliserons jsp pour demander /app/rest/server, mais il doit se terminer par .jsp. Dans cette situation, il existe même deux astuces que nous pouvons utiliser pour contourner cette vérification. Nous pouvons utiliser le point-virgule ; comme délimiteur de paramètre : /app/rest/server;.jsp, cette fois .jsp sera traité comme un second paramètre. La seconde astuce consiste à utiliser le fragment d'URI # : /app/rest/server%23.jsp. Ce qui vient après le fragment d'URI ne sera pas utilisé pour le routage, il sert uniquement à la navigation dans une page. Notez que le caractère doit être encodé en URL, sinon le navigateur l'ignorera d'abord.

Avec cette technique, nous pouvons même créer un nouvel utilisateur avec Privilège Administrateur. La documentation de TeamCity indique que nous pouvons créer un nouvel utilisateur via ce point de terminaison /app/rest/users.

Allons dans le panneau d'administration et vérifions si un nouvel utilisateur a été créé.

Comme prévu, un nouvel utilisateur avec Privilège Administrateur est créé. Avec le privilège administrateur, nous pouvons contrôler entièrement le serveur. Cependant, nous pouvons aller encore plus loin en obtenant une exécution de code à distance. Cette CVE est vulnérable dans toutes les versions antérieures à 2023.11.4 mais cette RCE n'est possible que pour les versions antérieures à 2023.11. Il existe un point de terminaison non documenté /app/rest/debug/processes qui permet à un utilisateur avec privilège administrateur d'exécuter des commandes arbitraires. Nous enverrons une requête POST avec 2 paramètres de requête dans l'URL de la requête. Pour Windows, ce sera ?exePath=cmd.exe¶ms=/c%20[notre commande ici] et avec Linux ce sera ?exePath=/bin/sh¶ms=-c%20[notre commande ici]. J'utilise TeamCity sous Windows, donc le chemin complet sera /app/rest/debug/processes?exePath=cmd.exe¶ms=/c%20whoami

Le serveur renvoie une erreur 403 indiquant que la requête manque de jeton CSRF. La documentation de TeamCity nous fournit également le point de terminaison pour récupérer le jeton, qui est /authenticationTest.html?csrf.
Après avoir récupéré le jeton, nous l'ajoutons à l'en-tête de la requête X-TC-CSRF-Token ou au paramètre HTTP tc-csrf-token, ici je choisis X-TC-CSRF-Token.

Après avoir fourni le jeton CSRF, nous exécutons avec succès la commande à distance, nous donnant un contrôle total sur le serveur.
Dans cette CVE, la faille de sécurité ne résidait pas dans la validation des entrées, mais dans la logique du code. Cette application est vraiment complexe, ce qui fait que les développeurs commettent parfois des erreurs. Nous apprenons également une nouvelle technique pour contourner l'authentification. J'espère que vous apprendrez quelque chose d'utile de cette analyse. Bon hacking !!!