前一篇文章整理了RMI调用的流程,大致分析了其中可能存在的反序列化攻击点,这篇来具体实现下这些攻击。但注意这些都是demo,存在被反打的风险,谨慎使用。
所有的反序列化攻击都需要被攻击方本地存在gadget,为了演示手动添加了CommonsCollections依赖。另外为了逻辑清晰本文只分析反序列化攻击,不涉及JNDI注入。
客户端/服务端攻击注册中心
首先看下攻击注册中心的方法,这里把客户端和服务端写在一起,因为从前面的流程已经分析过,实际上对注册中心来说并没有具体区分客户端和服务端,只是调用的函数不同罢了。调用lookup就代表是客户端,调用bind就代表是服务端。
既然要攻击注册中心,那么反序列化点自然是在注册中心里,也就是Registryimpl_Skel#dispatch。实际上这里面有反序列化点的都能打,先看看lookup。正常使用应该是客户端调用RegistryImpl_Stub里面的lookup
1 | public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { |
实际上ref.invoke就已经将客户端的数据传过去了,所以后面的代码对攻击不重要。这里有个问题,就是正常来说这个功能只接受字符串作为参数,那么skel那里也只会反序列化一个字符串,看上去是不能利用的。但实际上客户端已经获取到RegistryImpl_Stub了,也就获取到了里面的ref,自己实现一个lookup把恶意对象发过去就行了.
同样的道理,服务端攻击注册中心就是重新写个bind之类的,直接上代码,加个cc依赖弹计算器:
1 | public class RegistryExploit { |
bind那里也可以自己加一层代理变成Remote对象然后调用原生的bind方法。另外为了简单这个代码调用了UnicastRef#invoke,也就是实际上是可能被反打的。
注册中心攻击客户端
反过来注册中心也是可以打与它通信的对象,Registry_impl_Stub中的lookup和list两个方法调用了readObject,会反序列化查询到的Stub对象。那么在注册中心绑定恶意对象,客户端调用registry.lookup/list的时候就能攻击客户端:
1 | public class EvilRegistry { |
因为bind需要Remote对象,所以封装了一个类。这里虽然客户端上没有这个Wrapper类,但反序列化是从里往外的,在报错之前里面的恶意Map已经反序列化完成了。另外如果不发布一个真的远程对象程序就直接运行结束了,所以new了一个RemoteObjImpl。
而注册中心Stub中并没有显式的反序列化点可以攻击服务端。
客户端攻击服务端
之前分析客户端调用服务端远程对象的过程,服务端的UnicastServerRef#dispatch调用了unmarshalValue。那么客户端把参数设置成payload就能攻击了,当然前提是服务端接收的参数类型不能是基础类型。
如果接收参数是Object,那就很简单,直接传恶意对象就行了。但如果是String之类的,从代码上看也是能攻击的,但是不能直接传payload,那么看一下应该怎么写。
很容易想到,我在客户端重新定义一个接收Object的远程方法试试:
1 | public interface IRemoteObj extends Remote { |
将方法参数改成Object后,运行客户端,报错java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object
直接搜一下这个报错在哪,发现在UnicastServerRef#dispatch。
1 | public void dispatch(Remote obj, RemoteCall call) throws IOException { |
将这个hash值改成hashToMethod_Map里面的hash就行了。但正常来说这个hash值也是不好找的,所以尝试找到计算hash值的地方。跟一下客户端的调用流程,RemoteObjectInvocationHandler#invoke
1 | private Object invokeRemoteMethod(Object proxy, |
看到了ref.invoke传递的getMethodHash(method),那么在这改method就可以了。为了方便直接调试改值试试,这里好像默认用的是系统类加载器,改成用AppClassLoader才能找到自定义的接口。
调用remoteObj.sayHello(evilMap),在getMethodHash下断点
1 | methos = ClassLoader.getSystemClassLoader().loadClass("org.example.IRemoteObj").getDeclaredMethod("sayHello",String.class) |
修改后就能成功在服务端反序列化了。其实也可以和打注册中心一样重新写个invoke,反正最后都是调用UnicastRef#invoke。
1 | public static void main(String[] args) throws Exception { |
这个攻击场景的前提是知道服务端开放的远程方法,并且参数类型不是基础类型。
服务端攻击客户端
顺便看看反过来的情况,前面已经分析了,客户端会调用UnicastRef#invoke,其中对服务端返回的函数调用unmarshalValue对返回值进行了反序列化,那么在服务端返回一个恶意对象就可以攻击客户端。和客户端类似,如果返回值是Object的当然可以直接打,如果接口的返回值不是Object,就得重写一个服务端了。比较麻烦,实际意义不大,这里就不实现了。
DGC相关的攻击
上一篇文章分析过,DGC双向都有反序列化操作,先分析攻击DGC服务端。思路是获取DGCImpl_Stub对象,重写dirty方法,在DGCImpl_Skel#dispatch中触发反序列化,跟打注册中心的差不多,稍微麻烦点。
1 | public class DGCExploit { |
其实ysoserial中的exploit/JRMPClient是相同的功能,ysoserial里的实现方法是用socket重写jrmp协议。
同样也可以打DGC客户端,在DGCImpl_Stub触发反序列化,一样是意义不大,实现起来麻烦,因为打客户端可以用更通用的executeCall处的反序列化。
攻击JRMP客户端
前面分析过,只要客户端的stub发起JRMP请求,就会调用UnicastRef#invoke,也就会调用StreamRemoteCall#executeCall,导致被反序列化攻击。这里想实现攻击需要自己实现一个恶意服务端,把返回的异常信息改成payload,其实这就是ysoserial里面的exploit/JRMPListener实现的功能。具体实现大概就是从TCPTransport#run0拷过来,没用的删删,改改最后处理的地方。
具体使用是先启动监听
1 | java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc.exe |
然后客户端只要调用任意一个stub,触发UnicastRef#invoke就会被攻击,比如调用注册中心stub
1 | Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); |
所以如果扫到开放了1099端口就连,是有可能直接中招的。毕竟社会险恶。
这篇文章介绍了常用的针对RMI的攻击的具体实现。但随着JDK的安全更新,许多攻击方式也随之失效。针对这些安全加固措施,后面的文章会分析一些已有的绕过方式,以及个人所作的尝试。
参考链接
https://xz.aliyun.com/t/7930
https://github.com/wh1t3p1g/ysomap
https://mp.weixin.qq.com/s/M_-lWKb9xO6u2MxRaEQ--Q
https://su18.org/post/rmi-attack
http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/