WebLogic反序列化之CVE-2015-4852、CVE-2016-0638、CVE-2016-3510
WebLogic反序列化之CVE-2015-4852、CVE-2016-0638、CVE-2016-3510
CVE-2015-4852、CVE-2016-0638、CVE-2016-3510这3个CVE常常被拿到一起来讲,因为后2个CVE都是以CVE-2015-4852利用链为基础建立的。
按照我对反序列化利用链的理解,包含2个部分:
- 承担恶意代码的荷载Class
- 启动荷载Class的
启动Class或者启动机制
CVE-2015-4852的荷载Class就是较为熟知的Apache CC利用链,关于CC利用链的分析文章很多,就不在这里写了。放一篇我关于CC1的文章ysoserial gadget Commons-Collections1保姆级分析,核心就是ChainedTransformer、ConstantTransformer、InvokerTransformer。
关于承担恶意代码的荷载Class就到此为止,而怎么启动荷载,就需要先了解WebLogic的T3协议。因为WebLogic通过读取数据包里T3协议格式的序列化数据然后将其反序列化得到Object。
T3协议是WebLogic RMI使用的协议,是JRMP的强化版。
T3协议
WebLogic RMI就是WebLogic对Java RMI的实现,在功能和实现方式上稍有不同。我们来细数一下WebLogic RMI和Java RMI的不同之处。
- WebLogic RMI支持集群部署和负载均衡。
因为WebLogic本身就是为分布式系统设计的,因此WebLogic RMI支持集群部署和负载均衡也不难理解了。
- WebLogic RMI的服务端会使用字节码生成
(Hot Code Generation)功能生成代理对象。
WebLogic的字节码生成功能会自动生成服务端的字节码到内存。不再生成Skeleton骨架对象,也不需要使用UnicastRemoteObject对象。
- WebLogic RMI客户端使用动态代理。
在WebLogic RMI客户端中,字节码生成功能会自动为客户端生成代理对象,因此Stub也不再需要。
- WebLogic RMI主要使用T3协议(还有基于CORBA的IIOP协议)进行客户端到服务端的数据传输。
T3传输协议是WebLogic的自有协议,它有如下特点:
- 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
- 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。
关于T3协议我本来想找一下有没有详细的定义,但是百度Google都没有找到,直到看到SeeBug里有篇文章说因为WebLogic闭源的原因就没有T3协议规则这方面的资料。不过网上对应Poc利用部分的T3协议分析倒是有。
具体分析参考漏洞原理和weblogic t3 协议利用与防御
- 发现每个数据包里不止包含一个序列化魔术头。
(0xac 0xed 0x00 0x05) - 每个序列化数据包前面都有相同的二进制串。
(0xfe 0x01 0x00 0x00) - 每个数据包上面都包含了一个T3协议头。
- 仔细看协议头部分,发现数据包的前4个字节正好对应着数据包长度。
- 以及我们也能发现包长度后面的
01代表请求,02代表返回。
这些点说明了T3协议由协议头包裹,且数据包中包含多个序列化的对象。
现在知道了T3协议的格式,那么接下来有2种思路
-
替换数据包中多个序列化对象任意一个为恶意序列化数据。
-
weblogic发送的JAVA序列化数据的第一部分与恶意序列化数据进行拼接。
其实在我看来是一个意思,无非是选择替换第几个正常序列化部分。
网上的payload都是直接替换第1个序列化对象,我参考的Poc是这个Weblogic_direct_T3_Rces。
搞清楚怎么在T3协议数据流中加入恶意序列化数据后,接下来就可以说一说服务端是如何反序列化恶意数据并启动CC链。
从数据流到Object
Weblogic通过7001端口,获取到流量中T3协议的java反序列化数据。
通过调用栈,可以发现Weblogic最后通过ObjectInputStream读入序列化数据,并在readClassDesc(boolean unshared)方法中拿到类描述符来确定字节流中传递数据的类型,并交给对应的方法进行处理。
//ObjectInputStream.java
/**
* Reads in and returns (possibly null) class descriptor. Sets passHandle
* to class descriptor's assigned handle. If class descriptor cannot be
* resolved to a class in the local VM, a ClassNotFoundException is
* associated with the class descriptor's handle.
*/
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
ObjectStreamClass descriptor;
switch (tc) {
case TC_NULL:
descriptor = (ObjectStreamClass) readNull();
break;
case TC_REFERENCE:
descriptor = (ObjectStreamClass) readHandle(unshared);
break;
case TC_PROXYCLASSDESC:
descriptor = readProxyDesc(unshared);
break;
case TC_CLASSDESC:
descriptor = readNonProxyDesc(unshared); //进这里
break;
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
if (descriptor != null) {
validateDescriptor(descriptor);
}
return descriptor;
}
这里会走case TC_CLASSDES,在readNonProxyDesc()里通过readClassDescriptor()拿到描述符并作为参数传给resolveClass(readDesc)方法拿到相对应的Class对象,最后通过desc.initNonProxy()初始化。
//ObjectInputStream.java
/**
* Reads in and returns class descriptor for a class that is not a dynamic
* proxy class. Sets passHandle to class descriptor's assigned handle. If
* class descriptor cannot be resolved to a class in the local VM, a
* ClassNotFoundException is associated with the descriptor's handle.
*/
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
ObjectStreamClass desc = new ObjectStreamClass();
ObjectStreamClass readDesc = null;
readDesc = readClassDescriptor();
Class<?> cl = null;
try {
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {}
} catch (ClassNotFoundException ex) {}
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
return desc;
}
//InboundMsgAbbrev$ServerChannelInputStream.java
protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException {
Class var2 = super.resolveClass(var1);
return var2;
}
resolveClass()方法通过super.resolveClass(var1)拿到对应Class并返回。到此如何获得Class就分析结束了,接下来从ObjectInputStream.readClassDesc(boolean unshared)往上分析看看是如何通过Class得到Object的?
根据调用栈往上一层的是ObjectInputStream.readOrdinaryObject(boolean unshared)
//ObjectInputStream.java
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
ObjectStreamClass desc = readClassDesc(false);
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
}
return obj;
}
在readOrdinaryObject()中得到readClassDesc(false)返回的Class后desc.newInstance()将其实例化,
并且随后会调用Class对象相应的readObject()、readExternal、readResolve()方法。
这一部分参考从Weblogic原理上探究CVE-2015-4852、CVE-2016-0638、CVE-2016-3510究竟怎么一回事,非常精彩的一篇文章。
CVE-2015-4852
CVE-2015-4852利用的就是CC链,CC链的启动方法在上面也说了是readObject()方法,而如何触发readObject()方法也同样在上一部分的结尾说明了就是ObjectInputStream.readOrdinaryObject(boolean unshared),关注如下分支
//ObjectInputStream.java
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
}
会判断一下是否实现Externalizable接口,AnnotationInvocationHandler只实现了Serializable接口所以进入readSerialData(obj, desc),并关注invokeReadObject()方法。
//ObjectInputStream.java
/**
* Reads (or attempts to skip, if obj is null or is tagged with a
* ClassNotFoundException) instance data for each serializable class of
* object in stream, from superclass to subclass. Expects that passHandle
* is set to obj's handle before this method is called.
*/
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
slotDesc.invokeReadObject(obj, this);
}
进入invokeReadObject()方法,通过注释就知道是调用该Class对象的readObject()方法,在这里Class就是sun.reflect.annotation.AnnotationInvocationHandler。
/**
* Invokes the readObject method of the represented serializable class.
* Throws UnsupportedOperationException if this class descriptor is not
* associated with a class, or if the class is externalizable,
* non-serializable or does not define readObject.
*/
void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
}
🆗触发利用链,CVE-2015-4852结束。
CVE-2016-0638
正如最开始所说CVE-2016-0638建立在CVE-2015-4852的基础之上而来。为了防御CVE-2015-4852 WebLogic采用了黑名单把如下ban掉的方式。
weblogic.rjvm.InboundMsgAbbrev.class$ServerChannelInputStream
weblogic.rjvm.MsgAbbrevInputStream.class
weblogic.iiop.Utils.class
但是CVE-2016-0638找到了黑名单之外的weblogic.jms.common.StreamMessageImpl实现了Externalizable接口,所以在如下分支中会进入readExternalData()。
//ObjectInputStream.java
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
}
这回不需要看注释了,非常直接。
//ObjectInputStream.java
private void readExternalData(Externalizable obj, ObjectStreamClass desc)
throws IOException
{
obj.readExternal(this);
}
而StreamMessageImpl因为实现了Externalizable接口,所以需要实现readExternal()方法。
//StreamMessageImpl.java
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
super.readExternal(var1);
byte var2 = var1.readByte();
if (var3 >= 1 && var3 <= 3) {
switch (var3) {
case 1:
this.length = var1.readInt();
this.buffer = new byte[this.length];
var1.readFully(this.buffer);
ByteArrayInputStream var4 = new ByteArrayInputStream(this.buffer);
ObjectInputStream var5 = new ObjectInputStream(var4);
try {
while (true) {
this.writeObject(var5.readObject());
}
}
break;
}
}
}
在readExternal()方法里会调用var5.readObject(),而var5就是AnnotationInvocationHandler,再次RCE成功。
CVE-2016-3510
CVE-2016-3510的思路跟上一个一模一样。这回找到的是在黑名单之外的weblogic.corba.utils.MarshalledObject。而MarshalledObject利用的则是readResolve()方法,首先看看MarshalledObject的构造器。
//MarshalledObject.java
public MarshalledObject(Object var1) throws IOException {
if (var1 == null) {
this.hash = 13;
} else {
ByteArrayOutputStream var2 = new ByteArrayOutputStream();
MarshalledObjectOutputStream var3 = new MarshalledObjectOutputStream(var2);
var3.writeObject(var1);
var3.flush();
this.objBytes = var2.toByteArray();
}
}
可以看到构造器会将var1写入ByteArrayOutputStream然后toByteArray()转换为Byte数组并赋值给this.objBytes,接下来分析一下MarshalledObject的。
//MarshalledObject.java
public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
if (this.objBytes == null) {
return null;
} else {
ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
ObjectInputStream var2 = new ObjectInputStream(var1);
Object var3 = var2.readObject();
var2.close();
return var3;
}
}
可以看到在readResolve()方法中,会把this.objBytes反序列化并调用其readObject()方法。那么就很明了了,我们只要把CC利用链作为var1把MarshalledObject new出来然后调用readResolve()就好了。
重新看回readOrdinaryObject()。·hasReadResolveMethod()方法判断desc的readResolveMethod是否为null,如果为null则不进入if语句块。
//ObjectInputStream.java
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
}
return obj;
}
这回关注的是invokeReadResolve(obj)方法,跟进去看注释就好了。能进入就说明desc的readResolveMethod不为空,而且可以看到在invokeReadResolve(obj)方法中会通过readResolveMethod.invoke()来调用readResolve()方法。
//ObjectInputStream.java
/**
* Invokes the readResolve method of the represented serializable class and
* returns the result. Throws UnsupportedOperationException if this class
* descriptor is not associated with a class, or if the class is
* non-serializable or does not define readResolve.
*/
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
ObjectStreamClass desc = readClassDesc(false);
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
}
}
}
那么readResolveMethod属性为什么就是readResolve()方法呢?把目光放到java.io.ObjectStreamClass的构造器上
public class ObjectStreamClass implements Serializable {
/** class-defined readResolve method, or null if none */
private Method readResolveMethod;
/**
* Creates local class descriptor representing given class.
*/
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
//...
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
}
}
也就是说在构造ObjectStreamClass时会把cl的readResolve()方法赋值到readResolveMethod属性上。而cl自然就是MarshalledObject。
到此3个CVE都分析完了,可以说都不是很难。
参考文章: