WebLogic反序列化之CVE-2015-4852、CVE-2016-0638、CVE-2016-3510

WebLogic反序列化之CVE-2015-4852、CVE-2016-0638、CVE-2016-3510

CVE-2015-4852CVE-2016-0638CVE-2016-3510这3个CVE常常被拿到一起来讲,因为后2个CVE都是以CVE-2015-4852利用链为基础建立的。

按照我对反序列化利用链的理解,包含2个部分:

  1. 承担恶意代码的荷载Class
  2. 启动荷载Class启动Class或者启动机制

CVE-2015-4852的荷载Class就是较为熟知的Apache CC利用链,关于CC利用链的分析文章很多,就不在这里写了。放一篇我关于CC1的文章ysoserial gadget Commons-Collections1保姆级分析,核心就是ChainedTransformerConstantTransformerInvokerTransformer

关于承担恶意代码的荷载Class就到此为止,而怎么启动荷载,就需要先了解WebLogicT3协议。因为WebLogic通过读取数据包里T3协议格式的序列化数据然后将其反序列化得到Object

T3协议是WebLogic RMI使用的协议,是JRMP的强化版。

T3协议

WebLogic RMI就是WebLogic对Java RMI的实现,在功能和实现方式上稍有不同。我们来细数一下WebLogic RMIJava RMI的不同之处。

  • WebLogic RMI支持集群部署和负载均衡。

因为WebLogic本身就是为分布式系统设计的,因此WebLogic RMI支持集群部署和负载均衡也不难理解了。

  • WebLogic RMI的服务端会使用字节码生成(Hot Code Generation)功能生成代理对象。

WebLogic的字节码生成功能会自动生成服务端的字节码到内存。不再生成Skeleton骨架对象,也不需要使用UnicastRemoteObject对象。

  • WebLogic RMI客户端使用动态代理。

WebLogic RMI客户端中,字节码生成功能会自动为客户端生成代理对象,因此Stub也不再需要。

  • WebLogic RMI主要使用T3协议(还有基于CORBAIIOP协议)进行客户端到服务端的数据传输。

T3传输协议是WebLogic的自有协议,它有如下特点:

  1. 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
  2. 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。

关于T3协议我本来想找一下有没有详细的定义,但是百度Google都没有找到,直到看到SeeBug里有篇文章说因为WebLogic闭源的原因就没有T3协议规则这方面的资料。不过网上对应Poc利用部分的T3协议分析倒是有。

具体分析参考漏洞原理weblogic t3 协议利用与防御

  • 发现每个数据包里不止包含一个序列化魔术头。(0xac 0xed 0x00 0x05)
  • 每个序列化数据包前面都有相同的二进制串。(0xfe 0x01 0x00 0x00)
  • 每个数据包上面都包含了一个T3协议头。
  • 仔细看协议头部分,发现数据包的前4个字节正好对应着数据包长度
  • 以及我们也能发现包长度后面的01代表请求,02代表返回。

这些点说明了T3协议由协议头包裹,且数据包中包含多个序列化的对象。

现在知道了T3协议的格式,那么接下来有2种思路

  1. 替换数据包中多个序列化对象任意一个为恶意序列化数据

  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)返回的Classdesc.newInstance()将其实例化,

并且随后会调用Class对象相应的readObject()readExternalreadResolve()方法。

这一部分参考从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利用链作为var1MarshalledObject new出来然后调用readResolve()就好了。

重新看回readOrdinaryObject()。·hasReadResolveMethod()方法判断descreadResolveMethod是否为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)方法,跟进去看注释就好了。能进入就说明descreadResolveMethod不为空,而且可以看到在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时会把clreadResolve()方法赋值到readResolveMethod属性上。而cl自然就是MarshalledObject

到此3个CVE都分析完了,可以说都不是很难。

参考文章: