Java反序列化基礎篇-JDK動態代理
0x01 Java 的代理模式
先說說什麼是代理模式,要說代理模式,得從代理說起。下面一張圖中的中介,就是我們所說的代理。
1. 靜態代理
簡單理解靜態代理
- 以租客找中介向房東租房子為例
想要實現租客找中介租房東,在 Java 中就需要4個檔案,分別是房源、房東、中介、租客,其中房源應該是介面,其餘三項為類。
不明白房源為什麼是介面的師傅,這與 Java 程式設計的設計思想有關,我個人也喜歡把它與 c++ 裡面的純虛擬函式做類比。可以移步至狂神的視訊學習一下靜態代理。
- Rent.java:這是一個介面,可以抽象的理解為房源,作為房源,它有一個方法rent()為租房。
Rent.java
package src.JdkProxy.StaticProxy; // 租房的介面 public interface Rent { public void rent(); }
- Host.java:這是一個類,這個類就是房東,作為房東,他需要實現Rent.java這一個介面,並且要實現介面的rent()方法。
Host.java
package src.JdkProxy.StaticProxy; public class Host implements Rent { public void rent(){ System.out.println("房東要出租房子"); } }
- Client.java:這是一個啟動類,這個類其實就是租客,租客的想法也很簡單,就是找到中介,然後租房(為什麼不直接找房東呢?因為房東通常不想管那麼多事,而且房源基本被中介壟斷)
因為租客是要去找中介看房的,而不是去找房東看房的,所以我們這裡先把 Proxy.java 實現一下,也就是把中介相關的功能先實現一下。
Proxy.java:這是一個類,這個類是中介,也就是代理,他需要有房東的房源,然而我們通常不會繼承房東,而會將房東作為一個私有的屬性host,我們通過host.rent()來實現租房的方法。
Proxy.java
package src.JdkProxy.StaticProxy; // 中介 public class Proxy { private Host host; public Proxy(){} public Proxy(Host host){ this.host = host; } public void rent(){ host.rent(); } }
Client.java租客去找中介看房。
package src.JdkProxy.StaticProxy; // 啟動器 public class Client { public static void main(String[] args) { Host host = new Host(); Proxy proxy = new Proxy(host); proxy.rent(); } }
這樣子,基本的看房就完成了 ~
但是,租房這一過程就結束了嗎?
不可能啊,因為中介還要收中介費呢?
- 有一些行為是中介可以做的,而房東不能做的,比如看房,收中介費等等。所以我們要在 Proxy.java當中實現這些功能。
改進 Proxy.java
package src.JdkProxy.StaticProxy; // 中介 public class Proxy { private Host host; public Proxy(){} public Proxy(Host host){ this.host = host; } public void rent(){ host.rent(); contract(); fare(); } // 看房 public void seeHouse(){ System.out.println("中介帶你看房"); } // 收中介費 public void fare(){ System.out.println("收中介費"); } // 籤租賃合同 public void contract(){ System.out.println("籤租賃合同"); } }
優點:
- 可以使得我們的真實角色更加純粹 . 不再去關注一些公共的事情。
- 公共的業務由代理來完成 . 實現了業務的分工。
- 公共業務發生擴充套件時變得更加集中和方便。
缺點 :
- 一個真是類對應一個代理角色,程式碼量翻倍,開發效率降低。
我們想要靜態代理的好處,又不想要靜態代理的缺點,所以 , 就有了動態代理 !
深入理解靜態代理
深入到實際業務當中,比如我們平常做的最多的 CRUD
- UserService.java,這是一個介面,我們定義四個抽象方法。
package src.JdkProxy.MoreStaticProxy; // 深入理解靜態代理 public interface UserService { public void add(); public void delete(); public void update(); public void query(); }
我們需要一個真實物件來完成這些增刪改查操作。
UserServiceImpl.java
package src.JdkProxy.MoreStaticProxy; public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("增加了一個使用者"); } @Override public void delete() { System.out.println("刪除了一個使用者"); } @Override public void update() { System.out.println("更新了一個使用者"); } @Override public void query() { System.out.println("查詢了一個使用者"); } }
需求來了,現在我們需要增加一個日誌功能,怎麼實現!
- 思路1 :在實現類上增加程式碼 【麻煩!】
- 思路2:使用代理來做,能夠不改變原來的業務情況下,實現此功能就是最好的了!
處理手段:增加一個代理類來處理日誌。
UserServiceProxy.java
package src.JdkProxy.MoreStaticProxy; // 代理 public class UserServiceProxy implements UserService{ private UserServiceImpl userService; public void setUserService(UserServiceImpl userService) { this.userService = userService; } public void add() { log("add"); userService.add(); } public void delete() { log("delete"); userService.delete(); } public void update() { log("update"); userService.update(); } public void query() { log("query"); userService.query(); } // 增加日誌方法 public void log(String msg){ System.out.println("[Debug]使用了 " + msg +"方法"); } }
修改啟動器 Client.java
package src.JdkProxy.MoreStaticProxy; public class Client { public static void main(String[] args) { UserServiceImpl userService = new UserServiceImpl(); UserServiceProxy proxy = new UserServiceProxy(); proxy.setUserService(userService); proxy.add(); } }
如此一來,增加業務點的日誌便成功了 。
2. 動態代理
- 前文我們說到靜態代理的問題,還記得嗎?
每多一個房東就需要多一箇中介,這顯然不符合生活認知(對於租客來說,如果是用靜態代理模式,每當想要換一個房東,那就必須要再換一箇中介,在開發中,如果有多箇中介程式碼量就更大了)
動態代理的出現就是為了解決上面靜態代理的缺點。
動態代理的一些基礎知識
下面講的主要是一些原始碼的東西吧,不看也可。我是不建議看的,但是為了文章內容的完整性,我還是貼上來吧。
- 動態代理的角色和靜態代理的一樣。需要一個實體類,一個代理類,一個啟動器。
- 動態代理的代理類是動態生成的,靜態代理的代理類是我們提前寫好的。
JDK的動態代理需要了解兩個類
核心 : InvocationHandler 呼叫處理程式類和 Proxy 代理類
InvocationHandler:呼叫處理程式
public interface InvocationHandler
InvocationHandler是由代理例項的呼叫處理程式實現的介面
每個代理例項都有一個關聯的呼叫處理程式。
Object invoke(Object proxy, 方法 method, Object[] args);
當在代理例項上呼叫方法的時候,方法呼叫將被編碼並分派到其呼叫處理程式的invoke()方法。
引數:
- proxy– 呼叫該方法的代理例項
- method-所述方法對應於呼叫代理例項上的介面方法的例項。方法物件的宣告類將是該方法宣告的介面,它可以是代理類繼承該方法的代理介面的超級介面。
- args-包含的方法呼叫傳遞代理例項的引數值的物件的陣列,或null如果介面方法沒有引數。原始型別的引數包含在適當的原始包裝器類的例項中,例如java.lang.Integer或java.lang.Boolean。
Proxy : 代理
public class Proxy extends Object implements Serializable
Proxy提供了建立動態代理類和例項的靜態方法,它也是由這些方法建立的所有動態代理類的超類。
動態代理類 (以下簡稱為代理類 )是一個實現在類建立時在執行時指定的介面列表的類,具有如下所述的行為。 代理介面是由代理類實現的介面。 代理例項是代理類的一個例項。
public static Object newProxyInstance(ClassLoader loader, 類<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
返回指定介面的代理類的例項,該介面將方法呼叫分派給指定的呼叫處理程式。
引數
- loader– 類載入器來定義代理類
- interfaces– 代理類實現的介面列表
- h– 排程方法呼叫的呼叫處理函式
動態代理的程式碼實現
- 要寫動態代理的程式碼,需要抓牢兩個要點
①:我們代理的是介面,而不是單個使用者。
②:代理類是動態生成的,而非靜態定死。
我只能說這種程式設計思想是真的牛逼,其實我們還可以實現任意介面的動態代理實現,在這裡就不貼出來了。
首先是我們的介面類
UserService.java
package src.JdkProxy.DynamicProxy; public interface UserService { public void add(); public void delete(); public void update(); public void query(); }
接著,我們需要用實體類去實現這個抽象類
UserServiceImpl.java
package src.JdkProxy.DynamicProxy; public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("增加了一個使用者"); } @Override public void delete() { System.out.println("刪除了一個使用者"); } @Override public void update() { System.out.println("更新了一個使用者"); } @Override public void query() { System.out.println("查詢了一個使用者"); } }
接著,是動態代理的實現類
package src.JdkProxy.DynamicProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class UserProxyInvocationHandler implements InvocationHandler { // 被代理的介面 private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } // 動態生成代理類例項 public Object getProxy(){ Object obj = Proxy.newProxyInstance(this.getClass().getClassLoader(), userService.getClass().getInterfaces(), this); return obj; } // 處理代理類例項,並返回結果 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log(method); Object obj = method.invoke(userService, args); return obj; }
- 最後編寫我們的 Client,也就是啟動器
Client.java
package src.JdkProxy.DynamicProxy; import src.JdkProxy.DynamicProxy.UserServiceImpl; public class Client { public static void main(String[] args) { // 真實角色 UserServiceImpl userServiceImpl = new UserServiceImpl(); // 代理角色,不存在 UserProxyInvocationHandler userProxyInvocationHandler = new UserProxyInvocationHandler(); userProxyInvocationHandler.setUserService((UserService) userServiceImpl); // 設定要代理的物件 // 動態生成代理類 UserService proxy = (UserService) userProxyInvocationHandler.getProxy(); proxy.add(); proxy.delete(); proxy.update(); proxy.query(); } }
- 上述,我們的動態代理便完成了。
0x02 在反序列化中動態代理的作用
- 如果只是純講開發,沒什麼意義,我們重點來了,動態代理是如何參與反序列化攻擊的。
回到之前文章的內容,我們之前說要利用反序列化的漏洞,我們是需要一個入口類的。
我們先假設存在一個能夠漏洞利用的類為B.f,比如Runtime.exec這種。
我們將入口類定義為A,我們最理想的情況是 A[O] -> O.f,那麼我們將傳進去的引數O替換為B即可。但是在實戰的情況下這種情況是極少的。
回到實戰情況,比如我們的入口類A存在O.abc這個方法,也就是 A[O] -> O.abc;而 O 呢,如果是一個動態代理類,O的invoke方法裡存在.f的方法,便可以漏洞利用了,我們還是展示一下。
A[O] -> O.abc O[O2] invoke -> O2.f // 此時將 B 去替換 O2 最後 ----> O[B] invoke -> B.f // 達到漏洞利用效果
動態代理在反序列化當中的利用和readObject是異曲同工的。
readObject方法在反序列化當中會被自動執行。
而invoke方法在動態代理當中會自動執行。
0x03 參考資料
- http://www.bilibili.com/video/BV1mc411h719?p=9
- http://www.bilibili.com/video/BV1mc411h719?p=10
- http://www.bilibili.com/video/BV1mc411h719?p=11
- http://www.bilibili.com/video/BV16h411z7o9?p=3
- Spring中實現非同步呼叫的方式有哪些?
- 帶引數的全型別 Python 裝飾器
- 整理了幾個Python正則表示式,拿走就能用!
- SOLID:開閉原則Go程式碼實戰
- React中如何引入CSS呢
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫程式語言-遞迴函式是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多執行緒永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- LeCun再炮轟Marcus: 他是心理學家,不是搞AI的
- Java保證執行緒安全的方式有哪些?
- 19個殺手級 JavaScript 單行程式碼,讓你看起來像專業人士
- Python 的"self"引數是什麼?
- 別整一坨 CSS 程式碼了,試試這幾個實用函式
- 再有人問你什麼是MVCC,就把這篇文章發給他!