JDK原生反序列化利用鏈7u21

9eek發表於2021-09-27

前言

JDK 7u21以前只粗略的掃過一眼,一看使用了AnnotationInvocationHandler,就以為還是和 CC1 一樣差不多的利用方式,但最近仔細看了下利用鏈發現事情並不簡單~

7u21 要求你能理解:

  • TemplatesImpl 程式碼執行原理
  • 動態代理是什麼
  • AnnotationInvocationHandler 利用原理

其實7u21是對AnnotationInvocationHandler 的進一步挖掘。

呼叫鏈

  • HashSet.readObject()
  • map.put(k,v)。(k為代理物件)
  • k.equals(last_k) (last_k 為上一個put的元素)
  • k.equalImpl(). (觸發惡意invoker)
  • 反射執行 last_k.任意方法 (last_k 為惡意TemplatesImpl 即可程式碼執行)

image-20210927111332379

簡單描述就是:

LinkedHashSet 在反序列化初始化時會將物件重新一個一個put進內部資料結構table中,如果put的兩個元素的key的hash一樣,則要進行進一步的判斷,要呼叫後放入元素的equal方法去對比前面元素的值,而如果這個後面元素的key恰好是一個經過AnnotationInvocationHandler包裝的Templates動態代理物件,則在進行put時候會分別呼叫 AnnotationInvocationHandler 的hashImpl和equlImpl,恰好因為hashImpl 的計算會最終呼叫到equlImpl,通過equlImpl中的var5.invoke() ,反射執行Templates 的newTransformer方法,進而導致程式碼執行。

我們來看下yso的程式碼:

public Object getObject(final String command) throws Exception {
		final Object templates = Gadgets.createTemplatesImpl(command);

		String zeroHashCodeStr = "f5a5a608";

		HashMap map = new HashMap();
		map.put(zeroHashCodeStr, "foo");

		InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
		Reflections.setFieldValue(tempHandler, "type", Templates.class);
		Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

		LinkedHashSet set = new LinkedHashSet(); // maintain order
		set.add(templates);
		set.add(proxy);

		Reflections.setFieldValue(templates, "_auxClasses", null);
		Reflections.setFieldValue(templates, "_class", null);

		map.put(zeroHashCodeStr, templates); // swap in real object

		return set;
	}

最終的目標是要呼叫EvilTemples的newTransformer方法,在AnnotationInvocationHandler 中有個equalsImpl 方法:

 private Boolean equalsImpl(Object var1) {
        if (var1 == this) {
            return true;
        } else if (!this.type.isInstance(var1)) {
            return false;
        } else {
            Method[] var2 = this.getMemberMethods();
            int var3 = var2.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                Method var5 = var2[var4];
                String var6 = var5.getName();
                Object var7 = this.memberValues.get(var6);
                Object var8 = null;
                AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
                if (var9 != null) {
                    var8 = var9.memberValues.get(var6);
                } else {
                    try {
                        var8 = var5.invoke(var1);
                    } catch (InvocationTargetException var11) {
                        return false;
                    } catch (IllegalAccessException var12) {
                        throw new AssertionError(var12);
                    }
                }

                if (!memberValueEquals(var7, var8)) {
                    return false;
                }
            }

            return true;
        }
    }

拋開干擾程式碼存在一個反射執行的方法:

Method[] var2 = this.getMemberMethods();
 for(int var4 = 0; var4 < var3; ++var4) {
     Method var5 = var2[var4];
     AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
     if (var9 != null)
     {
         ....
     }else{
          var8 = var5.invoke(var1);
     }

其中getMemberMethods 來自type的所有方法:

image-20210926203620761

那意思就是equalsImpl能夠觸發一個傳入物件x的惡意方法,有沒有存在一個這樣的物件呢,當然是有的,那就是 TemplatesImpl,TemplatesImpl.newTransformer()和getOutputProperties() 都可以觸發惡意程式碼執行,其中asOneOfUs判斷是不是動態代理的class,所以只要保證var1 不是動態代理就ok。

所以我們就需要把type設定為TemplatesImpl,var1 也為TemplatesImpl物件即可,var1也即hashset的舊元素,為了能觸發惡意invoker,所以hashset的第二個元素要為一個經過AnnotationInvocationHandler包裝的物件。

也就是:

image-20210927185043978

步驟很清晰,有了前面幾篇文章的基礎後沒必要再分析TemplatesImpl和AnnotationInvocationHandler利用過程了,重點看下以下幾個問題

疑問

image-20210927191515100

  1. 上圖為Hashset put的原始碼,為了要執行euqals,就要保證map的兩個key hash一致,且兩個key不想等,key分別為templates和proxy,是兩個不同的物件,兩個hash是如何保持一致的?
  2. hash為0的字串的作用是啥?
  3. 為什麼要用LinkedHashSet,直接用HashSet行不行?

1. 兩個不相等的物件如何保證hashset.hash()後結果一致?

在7u21的反序列化執行載體hashset中,新增了兩個元素,一前一後分別是templates和proxy,雖然proxy物件是經過AnnotationInvocationHandler包裝的Templates 代理物件,但這兩個物件是不相等的,為了能執行後續步驟,讓流程執行到for迴圈下的if預計又必須保證兩個物件的hashcode一致才能進一步執行到euqals方法。

image-20210927192352475

如果只是普通的兩個Temples物件那必然是不相等的,但第二個元素proxy經過AnnotationInvocationHandler包裝的Templates物件,當proxy執行hashcode時,會直接執行handler.invoker函式:

image-20210927192702088

同時,判斷執行方法為hashcode則會跳轉到hashCodeImpl方法:

image-20210927192916023

跳轉到hashCodeImpl 時我們發現,關於proxy的hashcode計算已經和proxy本身沒有關係了,私有變數memberValues是一個map物件,圖片中的1會進行迭代,取出其中元素key和value 分別呼叫hashcode()進行異或運算然後乘以127 得到最後prxoy的hashcode值。

所以為了讓prxoy和templates 的hashcode一致,我們只要 構造一個map,使其滿足:

127 * key.hashcode ^ value.hashcode === templates.hashcode

這裡順便解釋了:

hash為0的字串的作用是啥?

那我們可以找到,因為物件的hashcode都是取的記憶體地址做一定的取整運算得到,所以如果硬湊兩個不相等物件做異或會很複雜,結果必然不固定,所以我們直接令 key.hashcode=0 127*key.hashcode 也就為0,讓value 直接等於templates 即可讓 proxy的hashcode等於templates,而f5a5a608 的hashcode就為0,所以這個map為:

map.put("f5a5a608",templates)

這樣就滿足了物件不相等,但hashcode卻是一樣的結果,也就能進行到euqals方法觸發temples.newTransformer() 方法。

為什麼要用LinkedHashSet,直接用HashSet行不行?

LinkedHashSet內部的資料結構是基於hashmap,所以理論上HashSet、LinkedHashMap、hashmap都可以,但因為類似我上面講的歡迎,對於物件的hash值,是根據物件保持在記憶體中的地址取整得到的,所以就會存在一定的隨機性,不管是序列化還是反序列化都無法保證proxy做為第二個元素被新增進去,也就沒發保證euqal方法會觸發proxy的invoker,無法執行templates的可執行方法。

我自己也做過實驗,使用hashmap作為載體會存在偶然無法程式碼執行的情況,所以為了保證穩定性可以使用帶順序的LinkedHashSet、或者LinkedHashmap,LinkedHashmap value設定為相同的內建基本型別即可。

image-20210927202217545

結語

由名字可知,jdk7u21 只能用於7u21以下的版本,因為在高版本中,在handler euqalmpl 中增加了對 傳入物件可執行方法對判斷,不能像過去一樣執行任意方法:

image-20210927204320413

公眾號

歡迎大家關注我的公眾號,這裡有乾貨滿滿的硬核安全知識,和我一起學起來吧!

相關文章