官方通告

https://help.fanruan.com/finereport/doc-view-4833.html

image-20251221231703220

从官方通告我们可以知道漏洞点在某个 export/excel 接口

漏洞分析

我们这里以11.0版本为例

image-20251221233517573

image-20251221233541233

一共找到两个版本,分别调用的Handler如下:

1
2
3
com.fr.nx.app.web.handler.export.largeds.LargeDatasetExcelExportHandler

com.fr.nx.app.web.v9.handler.handler.largeds.LargeDatasetExcelExportHandler

分别跟进看了一下,解析的逻辑大差不差

image-20251221233950528

image-20251221233929409

暂且不管,随便调一个往下看,这里先看v9

官方说是SQL注入漏洞,那么我们重点关注sql相关语句,跟到doHandle方法下个断点

image-20251221235145949

前两行分别是从请求当中的sessionID来获取一个模板对象以及对应的Calculator,主要是用于处理各种公式运算

image-20251222003007361

这里的sessionID是每个表格每次导出所对应的sessionID值,随便导出一个示例表格可以得到

随后跟进initCreator方法

image-20251221235836410

该流程一共需要三个参数(排除sessionID):

_parameters_

从参数中获取,json格式,结构满足如下条件:
image-20251222000158107

params

从参数中获取,xml格式,结构满足如下条件:

image-20251222000435587

functionParams

从参数中获取,json格式,结构满足如下条件:

image-20251222000523515

可以看到这3个参数都是可控的,而其中涉及到SQL查询的地方在下面位置

com.fr.io.exporter.excel.direct.WorkbookDataCreator#build

com.fr.io.exporter.excel.direct.WorkbookDataCreator#init

image-20251222000846337

image-20251222000906721

而init方法中有个很熟悉的字眼:renderSql方法,跟进

image-20251222001250819

image-20251222001440907

这个方法是某个nday中的漏洞点,可以处理一些帆软内置的函数,其中第二个参数为要处理的字符串,按道理来说只要能控制this.tableData.getQuery()的值就能执行内置函数,但我们跟进到这里会发现,query的值是不可控的

image-20251222001654313

image-20251222001835185

image-20251222002002726

这里的TableData只能从数据源中获取,而数据源我们只能控制dsName,所以这里这个方法是用不了的,那么还有哪里可以控制呢

回头看还有一个熟悉的字眼:

com.fr.nx.app.web.v9.handler.handler.largeds.LargeDatasetExcelExportHandler#dealParam

image-20251222003503278

巧了,正好就是某nday的触发点

com.fr.script.Calculator#evalValue(java.lang.String)

image-20251222003602650

刚刚我们提到com.fr.script.Calculator是处理公式运算,其中包含一些帆软内置函数的处理,这里使用的是帆软自己定义的一套表达式引擎

image-20251222004631505

继续跟进

image-20251222004753507

image-20251222004840437

通过parse方法对表达式进行解析,最后查找到对应的function类来进行处理

而我们从官方文档可以知道,帆软有个内置的sql函数可以用来执行语句,因此我们只需要控制这里的evalValue参数即可

image-20251222010208825

到这里我们其实可以看出来,这个漏洞并不像真正意义上的sql注入,而是帆软内置函数和表达式执行的问题,下面我们尝试利用

利用流程

控制evalValue参数

刚刚分析了整个流程,回头看一下怎么控制evalValue的参数

com.fr.nx.app.web.v9.handler.handler.largeds.LargeDatasetExcelExportHandler#dealParam

image-20251222010450876

共有两种情况,都与var17有关,而var17的来源是var3.getParameters(),即LargeDatasetExcelExportJavaScript.getParameters(),往回溯源

image-20251222010915253

image-20251222010932031

即从params参数中获取,也就是我们最开始分析到的3个参数,我们依次看一下怎么构造

__functionParams__构造

image-20251222011119889

没什么特殊的要求,满足json格式即可:{}

functionParams和params构造

image-20251222011324151

这里我们需要综合考虑,以及两种情况

第一种情况:

令var19为null,走if分支,还要保证var17的值是Formula的实例,这一步比较麻烦,所以我们优先考虑下面情况

第二种情况:

令var19不为null,走else分支,只需要让var17通过replaceAll处理后就可以执行

那么这种情况下的functionParams和params需要满足:

1
JSONObject var19 = (JSONObject)var8.get(var17.getName());
  • var8中存在键名和var17.getName()同名的键值对,并且键值也要满足json格式,不能为null
  • var17是遍历LargeDatasetExcelExportJavaScript实例中的parameters,所以xml需要存在Parameters节点

functionParams

满足json格式随便给个键值对:{“p”:{“x”:2}}

params

通过LargeDatasetExcelExportJavaScript获取parameters,而参数是在下面位置设置的

image-20251222012650396

image-20251222012915755

image-20251222012939404

readXMLObject方法是一个用状态机 + 回调的方式处理xml格式数据的框架方法,用于处理xml各个节点的数据,我们可以通过帆软本身的GeneralXMLTools.writeXMLableAsString方法来生成

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 com.fr.base.Formula;
import com.fr.base.Parameter;
import com.fr.general.GeneralUtils;
import com.fr.general.xml.GeneralXMLTools;
import com.fr.nx.app.web.handler.export.largeds.bean.LargeDatasetExcelExportJavaScript;
import com.fr.nx.app.web.v9.handler.handler.largeds.LargeDatasetExcelExportHandler;
import com.fr.stable.FormulaProvider;
import com.fr.stable.ParameterProvider;
import com.fr.stable.bridge.StableFactory;

import com.fr.base.Formula;
import com.fr.base.Parameter;
import com.fr.general.xml.GeneralXMLTools;
import com.fr.nx.app.web.handler.export.largeds.bean.LargeDatasetExcelExportJavaScript;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class Test {
public static void main(String[] args) {
Parameter p = new Parameter();
p.setName("p");
p.setValue("=1+1");

LargeDatasetExcelExportJavaScript js = new LargeDatasetExcelExportJavaScript();
js.setDsName("XXX");
js.setParameters(new Parameter[]{p});

String xml = GeneralXMLTools.writeXMLableAsString(js);
System.out.println(xml.replace("\\", ""));
System.out.println(urlEncode(xml));
}

public static String urlEncode(String str) {
str = str.replace("\\", "");

try {
return URLEncoder.encode(str, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return str;
}
}
}

其中Parameter中的name值和我们前面给的functionParams的值对应(p),内容为一个表达式(=1+1),dsName没有要求,默认有个ds1,实测随便传都可以

请求包构造

总结一下我们的三个参数以及sessionID:

1
2
3
4
5
6
7
sessionID:自行生成或通过sql注入获取

functionParams={"p":{"x":2}}

__parameters__={}

params:满足条件的XML

image-20251222014110344

构造请求包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET //webroot/decision/nx/report/v9/largedataset/export/excel?functionParams=%7B%22p%22%3A%7B%22x%22%3A2%7D%7D&__parameters__={} HTTP/1.1
Host: 118.24.121.59:65321
Cookie: JSESSIONID=3CB53ED924B36AF3EFA536936406EE64; tenantId=default; fine_remember_login=-2; fine_auth_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInRlbmFudElkIjoiZGVmYXVsdCIsImlzcyI6ImZhbnJ1YW4iLCJkZXNjcmlwdGlvbiI6ImFkbWluKGFkbWluKSIsImV4cCI6MTc2NzU0ODM4MywiaWF0IjoxNzY2MzM4NzgzLCJqdGkiOiJ1MnZFRmxaSlVnR0Q4WFU1SUxHK0ZqWU1Xc1pwNnBrVEpCVjllRE5hZ3o0UC81NkgifQ.X9Z3vswB_4XSNafAqlAc_MOKypxUvBzfQesfXnxxRic; sessionID=cad7b49e-0730-4443-9387-48851805943d
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInRlbmFudElkIjoiZGVmYXVsdCIsImlzcyI6ImZhbnJ1YW4iLCJkZXNjcmlwdGlvbiI6ImFkbWluKGFkbWluKSIsImV4cCI6MTc2NzU0ODM4MywiaWF0IjoxNzY2MzM4NzgzLCJqdGkiOiJ1MnZFRmxaSlVnR0Q4WFU1SUxHK0ZqWU1Xc1pwNnBrVEpCVjllRE5hZ3o0UC81NkgifQ.X9Z3vswB_4XSNafAqlAc_MOKypxUvBzfQesfXnxxRic
Accept-Language: zh-CN,zh;q=0.9
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
sessionID: cad7b49e-0730-4443-9387-48851805943d
Referer: http://118.24.121.59:65321/webroot/decision/v10/entry/access/a07d49da-fbe3-46f5-9b81-455d32b2ca8b?width=890&height=923
Accept-Encoding: gzip, deflate
params: xxxxxxxxxxxxx


下断点发包

image-20251222093643843

image-20251222014529049

跟进dealParam

image-20251222014830982

此时var19不为null,进入else分支,获取到的表达式为:=1+1

image-20251222015035505

image-20251222015421959

成功执行普通表达式,使用带内置函数的表达式方法类似,例如sql函数:

image-20251222020003212

过滤

在表达式执行的过程中,如果涉及到sql执行,会进行单独的校验处理

image-20251222020641633

image-20251222020914057

com.fr.cbb.dialect.security.JDBCSecurityChecker#check

image-20251222022535774

处理方式大家参考某nday就行了

结果

image-20251222093347377

EXP出于某些已知原因就不给出,xdm可以自己审一下