Contents

Atlassian Confluence CVE-2023-22515 分析

Contents

Atlassian Confluence CVE-2023-22515 分析以及一种 RCE? 方式

漏洞刚出来的时候就在看了, 但是这个洞公开的挺快的, 目前网上也有一些分析文章了

就简单记录一下自己根据官方通告复现漏洞的流程吧

官方以及 rapid 7 的漏洞通告:

https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html

https://www.rapid7.com/blog/post/2023/10/04/etr-cve-2023-22515-zero-day-privilege-escalation-in-confluence-server-and-data-center/

以 Confluence 8.5.1 和 8.5.2 为例

diff com.atlassian.confluence_confluence-8.5.1.jar 和 com.atlassian.confluence_confluence-8.5.2.jar

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121143025.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121144346.png

删除了 ServerInfoAction 和 ServerInfoFilter, 然后在 BootstrapStatusProviderImpl 获取的 applicationConfig 和 setupPersister 外面套了一层 ReadOnly

ReadOnlyApplicationConfig 和 ReadOnlySetupPersister 调用 setter 时会抛出异常

因为当时 rapid7 的通告说 /server-info.action 也能利用成功, 所以就先看了一会这个 ServerInfoAction 和 ServerInfoFilter

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121146636.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121206150.png

不过调试了一会发现这个 action 和 filter 并没有什么特别的地方

然后对于添加 ReadOnly 的这个操作, 稍微看一下代码就可以发现 applicationConfig 和 setupPersister 都有 setSetupType 和 setSetupComplete 这两个可能与 setup 有关的 setter

结合去年爆出的 CVE-2022-26134 OGNL 表达式注入 (虽然实际上没啥关系但当时确实第一时间想到了这个)

猜测大概是通过 OGNL 来修改 setupType 或者 setupComplete 这两个字段, 改变 Confluence 的 setup 状态, 使得我们可以通过 /setup/setupadministrator.action 路由去配置管理员用户

默认访问 /setup/setupadministrator.action 是没有用的

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121150282.png

具体逻辑位于 SetupCheckInterceptor

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121200286.png

这里的 isSetupComplete 其实跟 applicationConfig 的 setupComplete 字段有关

既然是跟 OGNL 相关的洞, 那么就需要找到一个 OGNL 的点, 这样才有机会利用

因为 Confluence 基于 Struts2, 自己对 Struts2 不是很熟, 所以就去通读了一遍 su18 师傅的 Struts2 漏洞分析文章

https://su18.org/post/struts2-1/

OGNL 中的根对象即为 ValueStack(值栈),这个对象贯穿整个 Action 的生命周期(每个 Action 类的对象实例会拥有一个 ValueStack 对象)。当Struts 2接收到一个 .action 的请求后,会先建立Action 类的对象实例,但并不会调用 Action 方法,而是先将 Action 类的相应属性放到 ValueStack 的实现类 OgnlValueStack 对象 root 对象的顶层节点( ValueStack 对象相当于一个栈)。在处理完上述工作后,Struts2 就会调用拦截器链中的拦截器,这些拦截器会根据用户请求参数值去更新 ValueStack 对象顶层节点的相应属性的值,最后会传到 Action 对象,并将 ValueStack 对象中的属性值,赋给 Action 类的相应属性。当调用完所有的拦截器后,才会调用 Action 类的 Action 方法。ValueStack 会在请求开始时被创建,请求结束时消亡。

我们需要找一个 OGNL 的点, 并且这个点能够以某种方式去调用某个类的 getter / setter, 以此来配置 applicationConfig 的 setupComplete 字段

看了一会发现跟 S2-020/S2-021/s2-022 的原理类似

https://su18.org/post/struts2-2/#s2-020s2-021s2-022

于是去 diff 跟 Struts2 有关的依赖, 即 com.atlassian.struts2_struts-support-1.1.0.jar 和 com.atlassian.struts2_struts-support-1.2.0.jar

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121154625.png

新版本修改了 SafeParameterInterceptor, 那么问题估计出在这

简单调试一下, 本地访问 http://127.0.0.1:8090/server-info.action?a.b.c=d

首先调用 SafeParametersInterceptor 的 doIntercept 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121230303.png

before -> filterSafeParameters -> isSafeParameterName

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121316195.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121231380.png

在这里会对传入的参数进行过滤, 首先匹配关键词 (actionErrors 和 actionMessages), 然后匹配两个正则, 最后还会调用 isSafeComplexParameterName 方法

这个方法的作用就是检查传入的参数是否调用了当前 action 的某个 getter / setter, 并检查该 getter / setter 本身或者其 returnType 是否使用了 @ParameterSafe 注解

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121332149.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121333683.png

如果方法本身和返回的类型都没有使用 @ParameterSafe 注解, 那么 isSafeMethod 就会返回 false, 最后就不会调用这个 getter / setter

但比较奇怪的地方在于它后面的语句是 super.doIntercept(invocation), 又调用了 Struts2 自带的 ParameterInterceptor 重新处理了一遍参数, 所以导致之前 SafeParametersInterceptor before 方法里面的过滤都没有用了

ParameterInterceptor doIntercept 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121238473.png

setParameters 方法会用 Struts2 自带的正则去过滤一遍参数, 具体逻辑位于 isAcceptableParameter 方法

当然这里的过滤并没有像上面的 SafeParametersInterceptor 那么严

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121239698.png

再往后就会通过 OGNL 相关的类去解析参数, 流程跟上面的文章差不多, 这里就不重复写了

总的来说, 因为 SafeParametersInterceptor doIntercept 方法的一些逻辑问题, 导致这个类自身对传入参数的过滤并没有生效, 我们最终还是可以通过 a.b.c=e 的形式去调用当前 action 的 getter / setter, 并不需要关心方法本身或者它的 returnType 是否使用了 @ParameterSafe 注解

那么后面的思路就很清晰了, 我们只需要去寻找 Confluence 里的某个 Action 类, 使得这个类本身或者它的父类存在可以被利用的 getter / setter 方法即可

以 ServerInfoAction 为例, 它继承自 ConfluenceActionSupport

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121156447.png

ConfluenceActionSupport 存在 getBootstrapStatusProvider 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121159329.png

获取到的是 BootstrapStatusProviderImpl, 而它存在 getApplicationConfig 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121159390.png

applicationConfig 存在 setSetupComplete 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121200905.png

最后根据层级关系构造参数即可

因为 Confluence 的所有 Action 都继承自 ConfluenceActionSupport, 所以理论上只要访问任意一个使用了 SafeParameterInterceptor 的路由, 无论是 GET 还是 POST 方法都能够利用成功

server-info.action

/server-info.action?bootstrapStatusProvider.applicationConfig.setupComplete=false

或者 login.action

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121252342.png

然后访问 /setup/setupadministrator-start.action 添加一个新的管理员用户

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121254220.png

漏洞的本质是因为在 Struts2 中可以通过参数去调用当前 Action 或者其父类的一些 getter / setter

根据这个特性可以构造出一个写 webshell 实现 RCE 的 payload, 不过这个 RCE 比较鸡肋 , 先来看下原理

关注 DefaultSetupPersister 的 setSetupType 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121352687.png

在这里会调用 saveApplicationConfig 方法 (实际调用的是 applicaionConfig 的 save 方法)

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121353574.png

首先获取 applicationConfig 的一些配置, 比如 setupType, buildNumber, properties

然后通过 configurationPersister 将其作为 configElement 添加

最后调用 configurationPersister.save 方法, 传入 applicationHome 和 configurationFileName

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121358916.png

save 方法最终会通过 XMLWriter 将配置文件写入到某个路径

根据上文可以知道写入文件的路径由 applicationHome 和 configurationFileName 确定, 前者代表 Confluence 的 home 目录, 后者代表配置文件名, 默认为 confluence.cfg.xml, 而且配置文件的内容部分可控, 比如可以控制 applicationConfig 内的 buildNumber 为某个自定义的字符串

那么我们就可以控制 buildNumber 为一个 JSP webshell, 并且指定 applicationHome 为 Confluence web 目录, configurationFileName 的文件后缀为 jsp, 从而写入一个 webshell

不过这里得解决几个问题

首先, 在使用 XMLWriter 写入数据时, JSP webshell 的 <> 字符会被过滤, 我的解决方法是利用 EL 表达式的标签 ${..} 绕过 (JSP 可以解析 EL 表达式)

其次, 必须得确定 Confluence 的 web 路径, 不过现在大部分的路径都是 /opt/atlassian/confluence/confluence, 而且在后台可以看到环境变量 (/admin/systeminfo.action 路由), 环境变量中会显示与 Confluence 相关的路径信息

最后, 写入 webshell 的时候可能会出现权限问题

目前 Confluence 主要有三种安装方式:

  • 通过 .bin 二进制程序安装
  • 基于 Docker 镜像直接运行
  • 下载 tar.gz 或 zip 压缩包, 解压缩后直接执行脚本启动 Confluence

如果基于 Docker 镜像运行 Confluence, 则默认会使用一个单独的 confluence 普通用户启动 Confluence 服务, 但是 Confluence 的 web 目录 /opt/atlassian/confluence/confluence 默认的所有者为 root, 权限为 755, 导致我们无法向 web 目录写入 webshell

如果使用 .bin 二进制程序安装, 则分为两种情况

  • 当前执行安装程序的用户为 root 用户, 则和上面一样, web 目录的所有者为 root, 权限为 755, 服务以 confluence 普通用户启动, 无法写入 webshell
  • 当前执行安装程序的用户为普通用户 (非 root 用户), 则会在该用户的 home 目录下安装 Confluence, 其 web 目录会变成 /home/用户名/atlassian/confluence/confluence, 此时目录的所有者就会变成当前的用户, 并且运行 Confluence 服务的用户也同样是这个用户, 那么就可以正常写入 webshell

如果是直接解压缩 tar.gz/zip 压缩包然后执行 bin/startup.sh 启动的 Confluence, 那这种情况就比较多了, 比如运维可能没有在解压缩之后限制 confluence 目录的所有者和权限, 又或者是直接以 root 权限运行的脚本, 我们其实仍然有机会去写入 webshell

最终 payload 形式如下

/server-info.action?bootstrapStatusProvider.applicationConfig.buildNumber=${Runtime.getRuntime().exec(param.cmd)}&bootstrapStatusProvider.applicationConfig.applicationHome=/Users/exp10it/Downloads/confluence-src/atlassian-confluence-8.5.1/confluence&bootstrapStatusProvider.applicationConfig.configurationFileName=shell.jsp&bootstrapStatusProvider.setupPersister.setupType=custom

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121800132.png

访问 shell.jsp

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202310121801845.png

当然, 总的来说, 这种写 webshell 实现 RCE 的方式还是比较鸡肋的, 就当是分享一种思路了