JAVA RMI反序列化&JNDI注入漏洞利用

什么是RMI?

在说RMI 之前,需要理解两个名词:对象序列化RPC

1. 序列化:
一个类的对象要想序列化成功,必须满足两个条件:
1.该类必须实现 java.io.Serializable 对象。
2.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。被声明为transient的属性不会被序列化。

如果你想知道一个 Java 标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现java.io.Serializable接口。

2. RPC:
RPC即Remote Procedure Call,可以理解成远程过程调用。RPC只是一个概念,不要纠结于他的实现形式,甚至JSON都可以实现一个RPC协议。

RMI:
二者结合,就成了RMI->Remote Method Invocation,即远程方法调用,它可以被看作是RPC的Java版本。它使客户机上运行的程序可以调用远程服务器上的对象。看代码比较好懂。

RMI实现(非Spring方式):

  1. 实现一个可序列化类。
  2. 定义一个接口,这个接口需要继承Remote接口,这个接口中的方法必须声明RemoteException异常。
    1
    2
    3
    4
    5
    6
    import java.rmi.*;
    import java.util.List;

    public interface SerializeService extends Remote{
    List<Serialize> getList() throws RemoteException;
    }
  3. 创建一个类实现2中的接口,还需要继承UnicastRemoteObject类,并显示声明无参构造函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    import java.util.ArrayList;
    import com.sun.xml.internal.bind.v2.schemagen.xmlschema.List;

    public class SerializeServiceImpl extends UnicastRemoteObject implements SerializeService{
    public SerializeServiceImpl() throws RemoteException{};

    public List<Serialize> getList() throws RemoteException{
    List<Serialize> array = new ArrayList<Serialize>();
    Serialize ser1 = new Serialize();
    ser1.setAge(10);
    ser1.setName("lfy");
    array.add(ser1);
    return array;
    }
    }
  4. 创建并启动RMI服务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.rmi.Naming;
    import java.rmi.registry.LocateRegistry;

    public class StartService{
    public StartService(){};

    public static void main(String[] args){
    try {
    SerializeServiceImpl serializeService = new SerializeServiceImpl();
    LocateRegistry.createRegistry(5005);
    Naming.rebind("rmi://127.0.0.1:5005/SerializeService", serializeService);
    System.out.println("Server Start!");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  5. 创建客户进行RMI调用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.rmi.Naming;
    import java.util.List;

    public class Client{
    public Client(){};
    public static void main(String[] args){
    try {
    SerializeService serializeService = (SerializeService) Naming.lookup("rmi://127.0.0.1:5005/SerializeService");
    List<Serialize> serList= serializeService.getList();
    for(Serialize s: serList){
    System.out.println("Name: "+s.getName()+" Age: "+s.getAge());
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

我的理解:
其实RMI从远程拿到对象,就好像Spring 从Bean里拿到对象一个道理,就是从别的地方拿到需要的数据。RMI通信传输的数据是经过序列化的数据,
具体可看我抓到的包。

图1

JAVA RMI 反序列化远程命令执行漏洞分析 ?

需要知道的背景知识

  1. 通过 JNDI 的接口就可以存取 RMI Registry/LDAP/DNS/NIS 等所谓 Naming Service 或 DirectoryService 的内容
    JNDI的使用目的,最根本的就是java应用通过一个名字获取其他JVM中的数据。而在提供JNDI服务的服务端应用中,建立了一个类似键值对的形式,存储JNDI的名字和数据的绑定。这就类似于数据库的连接池,不必每次去连接数据库都重新建立一个连接,而是直接从连接池中获取已有连接拿来使用即可,节省了内存同时也优化了效率。
    引用网上的一个很形象的解释:

    1
    2
    jndi就象是url,记录着你要访问的地址
    而rmi就象是http或是tcp,是一种连接方式

    因此,当lookup可控时,我们可以通过JNDI注入的方式实现RCE。

  2. JNDI API 中涉及到的常见的方法与接口的作用,如:Context.lookup
    一个简单的JNDI RMI Server demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class RmiJndiSever {  
    public static void main(String[] args) {
    try {
    //注册RMI服务器端口
    LocateRegistry.createRegistry(8080);
    //建立RMI服务端接口实现对象
    RmiSimple server = new RmiSimpleImpl();
    //设置JNDI属性
    Properties properties = new Properties();
    //RMI的JNDI工厂类
    properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
    //RMI服务端的访问地址
    properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:8080");
    //根据JNDI属性,创建上下文
    InitialContext ctx = new InitialContext(properties);
    //将服务端接口实现对象与JNDI命名绑定,这个地方写的并不是很规范
    //如果在J2EE开发中,规范的写法是,绑定的名字要以java:comp/env/开头
    ctx.bind("RmiSimple", server);
    System.out.println("RMI与JNDI集成服务启动.等待客户端调用...");
    } catch (RemoteException e) {
    e.printStackTrace();
    } catch (NamingException e) {
    e.printStackTrace();
    }
    }
    }

1. RMI反序列化漏洞利用条件

主要有两个条件:

1.存在反序列化传输。比如CVE-2017-3241

2.存在有缺陷的第三方库如commons-collections,或者利用java本身漏洞,

注意rmi可以打服务端registry也可以客户端lookup,两种都会反序列化
https://paper.seebug.org/1091/

2. JNDI注入利用条件

  1. lookup参数可控。
  2. InitialContext类及他的子类的lookup方法允许动态协议转换
  3. lookup查找的对象是Reference类型及其子类
  4. 当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。

存在接口可以进行对象反序列化
访问对象可以出网,因为要进行远程类下载(内网中另作讨论)
目标对象中的CLASSPATH中存在Spring-tx-xx.jar有缺陷类的jar包

那Reference为何会找远程的class下载呢?

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

至于RMI的反序列化过程,参考这篇https://xz.aliyun.com/t/2223。同时还可以看一下rmi的原理,和java的代理。

绕过高版本JDK的限制进行JNDI注入利用

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

1
攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用。

通过这篇文章,我们发现,上边我们rmitest1 demo使用的就是通过Reference返回一个对exp对象的引用,然后客户端利用时通过factory将其转为对象实例。
同样通过这篇文章,如果我们把exp类从本地文件中删除,就不能rce了,这是因为我本地8u191版本会限制。用tomcat el即可绕过。还有个问题就是,demo中的代码不会去请求下载class文件,哪怕我把限制的两个属性设置true也不行,这个不懂。。

RMI各种反序列化

这里补充下,我也就不写了,有人写的非常全了。
https://blog.0kami.cn/2020/02/06/rmi-registry-security-problem/
https://xz.aliyun.com/t/7930

Proudly powered by Hexo and Theme by Hacker
© 2021 LFY