之后多写漏洞分析
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 漏洞影响
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
1 | version: '2' |


配置调试环境

1 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 |
上容器中修改 /opt/atlassian/confluence/bin/setenv.sh文件,加 jvm 参数,然后重启web容器:
1 | CATALINA_OPTS="${CATALINA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" |

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

0x03 漏洞验证和利用
nuclei运行poc:
1 | 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 |

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

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

0x04 漏洞分析
主要分析几点
- 为什么访问 /json/setup-restore.action 相当于访问 /setup/setup-restore.action
- 为什么访问/json/setup-restore.action不需要登录,即为什么能未授权访问
- 能未授权访问 /json/setup-restore.action之后该如何利用,怎么构造poc,怎么实现rce
/setup 下的 action通过 /json 都能访问
开始调试 /json/setup-restore.action
参考:https://evilpan.com/2023/11/01/struts2-internal/#debugging-tips
发包:
1 | nuclei.exe -t json-setup-restore-pure-post.yaml -u http://192.168.88.132:8090 |
1 | id: json-setup-restore-pure-post |
在 com.opensymphony.xwork2.config.impl.DefaultConfiguration 的 findActionConfigInNamespace() 下断点:

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

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

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

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


测试一下这个断点的位置 /json 这个namespace 里面有没有想要的action :
1 | namespaceActionConfigs.get("/json").get("setup-restore") |
可以看到是有的:

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

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

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

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

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

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


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

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

不用登录就能访问 /json/setup-restore.action
开始调试
为了调试这个过程,需要发另外的包:
1 | nuclei.exe -t debug2.yaml -u http://192.168.88.132:8090 |
1 | id: confluence-json-setup-restore-restore-system |
参考 https://xz.aliyun.com/t/12981 和 https://evilpan.com/2023/11/01/struts2-internal/
执行 action 的起点,可以认为是 org.apache.struts2.dispatcher.filter.StrutsExecuteFilter 的 doFilter() 方法:

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

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

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

所有 interceptor 如下:
1 | [profiling] => [com.atlassian.xwork.interceptors.XWorkProfilingInterceptor] |
而其中的一些,会通过调用 actionInvocation.getAction() 获取本次请求所对应的 action 对象,然后判断当前对这个 action 的请求是否合法
做这样鉴权工作的 interceptor 主要有以下几个:
在 Confluence 中,一个 Action 有两个重要的父类
com.atlassian.confluence.core.ConfluenceActionSupport和com.atlassian.confluence.core.ActionSupport其中定义了一些confluence中的action比较特别的接口
1 | com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor |
调试 ConfluenceAccessInterceptor

跟入,返回true:

调试 PermissionCheckInterceptor
调用了 action 的 isPermitted() 来判断是否合法:
这个
isPermitted()重载自com.atlassian.confluence.core.ConfluenceActionSupport

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

调试 WebSudoInterceptor
调试其 intercept() 方法,

来到 webSudoManager.matches(requestURI, actionClass, actionMethod) 对一些 管理员相关操作做校验,跟入其中,可以看到先会判断访问的路由是否以 /admin/ 开头:
1 | public boolean matches(String requestURI, Class<?> actionClass, Method method) { |

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

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

而对于 SetupRestoreAction 而言,主要是判断一下传上来的zip是不是符合一定的格式,有没有一些必要的项:
1 | 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 |

小结
所访问的 /json/setup-restore.action 不受 interceptor 鉴权的影响
poc 构造
POST 请求访问 /json/setup-restore.action 接口:


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

1 | POST /json/setup-restore.action?synchronous=false |
因而需要想办法构造用于恢复的zip
构造 zip
zip 的结构有一定要求,需要满足 com.atlassian.confluence.importexport.actions.SetupRestoreAction 的 validate() :

这里的思路是从这个 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 :

确定可疑的方法:
1 | com.atlassian.confluence.DefaultExportContext.importexport.getXmlBackupInstance() |
最终确定从 com.atlassian.confluence.importexport.actions.BackupAction 的 execute() 出发,可以构造一个满足条件的 zip :



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

点击 导出:

然后 用 docker cp 命令把 创建的 zip 拿出来就可以了
构造exp
在构造exp的时候发现需要传一个 atl_token 否则没法成功利用,所以先访问 /json/setup-restore.action 获取一个 atl_token 然后利用:
1 | id: confluence-json-setup-restore-restore-system |
因为restore的过程可能需要10~20秒左右,所以设置一个 timeout 在 30 秒以内收到 response 就可以(如果不加的话nuclei默认的timeout是10秒):
1 | 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
成功替换目标站点之后,可以访问管理员后台:

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

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

0x05 漏洞修复
Confluence 每次升级之后,jar包的名字可能会改,主要的更改之处在于jar包末尾的版本号,为了 diff 方便,可以把两个版本 confluence 的所有 jar 包的版本号都去了,然后直接拖到 idea 里面做对比:
1 | import os |
可以看到,修复方式是给 SetupRestoreAction 加了两个 annotation @WebSudoRequired 和 @SystemAdminOnly ,这样一来,在 WebSudoInterceptor 鉴权的时候,就会要求管理员登录:
