Java反序列化基礎篇-JDK動態代理

語言: CN / TW / HK

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 參考資料

  • https://www.bilibili.com/video/BV1mc411h719?p=9
  • https://www.bilibili.com/video/BV1mc411h719?p=10
  • https://www.bilibili.com/video/BV1mc411h719?p=11
  • https://www.bilibili.com/video/BV16h411z7o9?p=3