之后多写漏洞分析
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
鉴权的时候,就会要求管理员登录: