初识Java 反序列化
初识Java 反序列化
What
什么是序列化与反序列化
- java 序列化可以将一个对象序列化成JVM认识的字节序列,字节序列中包含了对象的数据,主要以对象属性为主。
- java 反序列化是指把字节序列恢复为java对象的过程。利用在A平台上序列化产生的字节序列,可以在B平台上反序列化出同样的对象。
Why
为什么要用序列化与反序列化
- 实现了数据的持久化,通过序列化可以把数据永久的保存在硬盘上。
- 利用序列化实现远程通信,即在网络上传送对象的字节序列。
How
如何实现序列化与反序列化
JDK类库中序列化API
使用到JDK中关键类 ObjectOutputStream(对象输出流) 和ObjectInputStream(对象输入流)
- ObjectOutputStream 类中:通过使用 writeObject(Object object) 方法,将对象以二进制格式进行写入。
- ObjectInputStream 类中:通过使用 readObject()方法,从输入流中读取二进制流,转换成对象。
序列化一个对象
被序列化的类必须要实现 Serializable 接口,否则将无法序列化对象。
1 | public class User implements Serializable { |
1 | public class Main { |
序列化对象时使用对象输出流"ObjectOutputStream",往指定输出流里写入 User 对象,使用控制台,可以直观的看到对象序列化后的样子。
�� sr Simple.User��Gm|X I ageL namet Ljava/lang/String;xp t whippet
反序列化对象
将对象序列化成byte数组。再用"ObjectInputStream"反序列化回来。
1 | public class Main { |
输出这个反序列化后的对象,和一开始为 User 对象设定的属性值完全相同。
同时Java为我们提供了自定义writeObject()、readObject()方法的功能,我们在目标类中自定义writeObject()、readObject()方法之后,将会首先调用我们自定义的方法,然后在继续执行原有的方法步骤.
1 | public class User implements Serializable { |
User 类写一个 readObject 方法上去,当对象被反序列化的时候,该方法就会被调用。
序列化时仅能序列化对象的属性,并不能控制方法中的reObject的代码,所有漏洞的构造需要引入新的jar包
commons-collections-3.1反序列化漏洞
漏洞利用
利用maven 安装依赖
1 | <dependencies> |
1 | import org.apache.commons.collections.Transformer; |
漏洞分析
java.lang.Runtime#exec(java.lang.String)
在exec处添加断点 查看调用链
最终遍历了"outerMap"这个Map对象的 Entry 集合,然后执行了 Entry 对象的的 setValue 方法导致执行了反射链。
所以先查看 outerMap 的实现类
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
调用了方法org.apache.commons.collections.map.TransformedMap#decorate
这个静态方法又去new了一个TransformedMap对象。map传入的是一个普通的数据,valueTransformer则是构造的调用链。
org.apache.commons.collections.map.TransformedMap#TransformedMap
实例化对象成功后依次调用了四个方法
- entrySet()
- iterator()
- next()
- setValue()
在类 TransformedMap中未找到定义的 entrySet()方法,在他的父类AbstractInputCheckedMapDecorator 中找
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator#entrySet
new 了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySet
EntrySet类中的iterator方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySet#iterator
又new了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySetIterator
EntrySetIterator类中的next方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.EntrySetIterator#next
又new了一个内部类
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry
MapEntry类中的setValue方法
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue
第一行调用了checkSetValue方法,此处的parent来自
所以实际上调用为 org.apache.commons.collections.map.TransformedMap#checkSetValue
调用了valueTransformer 的transform方法
所以首先调用了org.apache.commons.collections.functors.ChainedTransformer#transform
循环调用数组每一个对象的transform方法,并传入object对象,再将执行结果赋值给object对象
[0]是ConstantTransformer对象,它会返回new时候的参数中的Object对象,这里也是就是"java.Runtime"
[1]-[3]是InvokerTransformer对象,调用的是反射的代码
iTransformers的值是在初始化构建ChainedTransformer时生成
org.apache.commons.collections.functors.ChainedTransformer#ChainedTransformer
ConstantTransformer类中实现transform方法
org.apache.commons.collections.functors.ConstantTransformer#transform
InvokerTransformer类中实现了transform方法
org.apache.commons.collections.functors.InvokerTransformer#transform
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
(“getMethod”, new Class[] {String.class, Class[].class }, new Object[] {“getRuntime”, new Class[0] })
iMethodName = “getMethod”;
iParamTypes = new Class[] {String.class, Class[].class } ;
iArgs = new Object[] {“getRuntime”, new Class[0] }
1 | public Object transform(Object input) { |
- 第一个参数"getMethod"是这个函数的名字
- 第二个参数new Class[]{String.class, Class[].class}是getMethod的2个参数参数类型,一个是String,一个是class[]
- 第三个参数new Object[]{“getRuntime”, new Class[0]}是getMethod的2个参数值,一个是getRuntime,一个是空,因为是数组形式所以要这么写
getMethod(<String> “getRuntime”, <Class[]> null)
invoke(<Object>null, <Object[]>null)
exec(<String>"“calc.exe”")
就是通过反射去调用Runtime.getRuntime().exec(“calc.exe”)
Java反射机制
java中执行系统命令的方法为
1 | public class Exec { |
正常的执行步骤为
1 | public class Exec { |
相对应的反射代码为
1 | public class Exec { |
Method Class.getMethod(String name, Class<?>… parameterTypes)的作用是获得对象所声明的公开方法。该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes是按声明顺序标识该方法形参类型。 getMethod(方法名, 方法类型)
Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
等价于
Object runtime = java.lang.Runtime.getRuntime()
- person.getClass().getMethod(“Speak”, null);
获得person对象的Speak方法,因为Speak方法没有形参,所以parameterTypes为null - person.getClass().getMethod(“run”, String.class);
获得person对象的run方法,因为run方法的形参是String类型的,所以parameterTypes为String.class
Method Class.invoke(Object obj, Object… args)
invoke(某个对象实例, 传入参数)
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc");
等价于
runtime.exec("calc");
调用生成的runtime实例的exec方法,并将"clac"参数传入exec()方法
实际利用
对象是可以被反序列化,但并不在反序列化时触发调用链,而是要经过迭代器迭代并且使用 setValue() 方法才行,正常情况下基本不会有这种场景。
sun.reflect.annotation.AnnotationInvocationHandler该类中重写了readObject方法,在被调用时会执行setValue操作, 如果能把TransformedMap装入这个AnnotationInvocationHandler类就可以实现任意代码执行
利用代码
1 |
|
java.lang.Runtime#exec(java.lang.String)
在exec处添加断点 查看调用链
sun.reflect.annotation.AnnotationInvocationHandler#readObject
传入的第一个参数 var1 必须为继承Annotation的子类,Annotation这个接口是所有注解类型的公用接口,所有注解类型都实现了这个接口。
- java.lang.annotation.Retention.class
- java.lang.annotation.Target.class
否则在这个地方就return,不会继续向下执行了
传入的第二个参数 var2 则是构造的调用链
同时map的键值必须为"value",否则利用不成功,这是一处小细节~
Java 反序列化漏洞始末(1)— Apache Commons
浅显易懂的JAVA反序列化入门
手把手教你写JAVA反序列化的POC