ysoserial java 反序列化 Groovy1

ysoserial簡介

ysoserial是一款在Github開源的知名java 反序列化利用工具,裏面集合了各種java反序列化payload;

由於其中部分payload使用到的低版本JDK中的類,所以建議自己私下分析學習時使用低版本JDK JDK版本建議在1.7u21以下。

此篇文章爲java反序列化系列文章的第一篇,後續會以ysoserial這款工具爲中心,挨個的去分析其中的反序列化payload和gadget,講完該工具後會繼續對工具中沒有的java 反序列化漏洞進行講解,例如 FastJson JackSon,WebLogic等等,並將這些漏洞的exp加入到ysoserial中然後共享和大家一起學習交流。

源碼下載地址

https://codeload.github.com/frohoff/ysoserial/zip/master

jar包下載地址

https://jitpack.io/com/github/frohoff/ysoserial/master-30099844c6-1/ysoserial-master-30099844c6-1.jar

 

源碼深度解析

我們首先看一下該payload的整個源碼

代碼量其實很少,但是調用了一些別的類中的方法,看起來可能不是太直觀,我將調用方法中所做的操作都寫到一個類的main方法中這樣看起來應該會更好理解一些。

先寫一個簡化版的可以執行代碼的Demo

直接運行的話會執行我們預先設定好的命令,調用計算器

但是這短短几行代碼裏我們並沒有調用Runtime對象的exec方法亦或着ProcessBuilder對象的star方法來執行命令,我們僅僅調用了一個Map對象的entrySet()方法,怎麼就可以執行命令了呢?

對java有些許瞭解的同學應該熟悉Map.Entry是Map裏面的一個接口,主要用途是來表示Map對象中的一個映射項也就是一個<key,value> 並提供了以下五個方法,通常我們會使用map.entrySet().iterator(),方法得到一個Iterator對象從而對Map中的Entry對象進行遍歷,那爲何一個獲取遍歷對象的操作會導致代碼執行呢?這裏就涉及到這個Map對象究竟是哪一個實現了Map接口的類來實例化的。

首先我們先來看看這個map變量裏面保存的是什麼

居然是一個代理對象,這裏就要涉及到java的一個知識點,就是所謂的動態代理。動態代理其實不難理解,可以寫個簡單的例子,以下是這個例子的代碼。

我們可以看到我們寫了一個Son類繼承了Father接口,然後在Test3類中的main方法中被實例化,接下來我們通過Proxy的newProxyInstance方法生成了一個Son對象的代理,我們傳遞了三個參數進去,Son類的類加載器和實現的接口,這裏注意被代理的對像是一定要實現至少一個接口的,因爲實例化的代理類本身是繼承了Proxy類,所以只能通過實現被代理類接口的形式來實例化。最後我們通過匿名內部類的形式傳入了一個InvocationHandler對象,InvocationHandler是一個接口,該接口中只有一個方法就是invoke方法,所以我們一定要重寫該方法。

然後我們看執行結果

可以看到,我們調用Son對象本身和Son的代理對象所執行的結果是不同的,因爲代理對象在執行被代理對象的任意方法時,會首先執行我們之前重寫的InvocationHandler的invoke方法。同時會傳入三個參數,第一個參數是代理對象本身,第二個參數是你要執行的方法的方法名,第三個參數是你要執行的該方法時要傳遞的參數。關鍵點在於什麼?在於無論你調用代理對象的那一個方法,都一定要先執行這個Invoke方法。

然後返回到我們之前的payload中我們可以看到我們使用Proxy.newProxyInstance方法生成了一個代理對象,然後將其強轉成了一個Map對象然後調用了entrySet方法。

接下來我們先記住我們payload所用到的兩個類也就是所謂的gadget

是位於org.codehaus.groovy.runtime包下的ConvertedClosure和MethodClosure。

接下來我們就來一步一步的調試分析

首先我們生成一個MethodClosure對象並將我們要執行的命令和和一個值爲“execute”的字符串傳遞進去我們跟進

可以看到我們將要執行的命令傳給了MethodClosure的父類來處理,將“execute”賦值給了MethodClosure.method屬性。然後緊接着跟到Closure的構造方法中看到命令被賦值給了Closure.owner和Closure.delegate屬性,之所以講這些賦值過程是因爲後面都會用得到。

接下來payload中又實例化了另一個對象並將剛纔實例化的MethodClosure對象和一個字符串常量“entrySet”傳入,我們同樣繼續跟進。

字符串常量被賦值給ConvertedClosure.methodName屬性

MethodClosure對象賦值給父類的的ConversionHandler.delegate屬性

接下這兩步就是生成一個Class類型的Arry數組因爲Proxy.newProxyInstance方法第二個參數是動態代理類要實現的接口要以數組的形式傳入。所以我們生成了一個Class數組並在其中存入我要實現的接口也就是Map.calss

接下來就是生成動態代理對象的過程了,這個在前面已經介紹過了,Proxy.newProxyInstance方法傳遞的第二個參數是代理類所要實現的接口,裏面只有一個Map.class所以生成的代理對象是實現了Map接口裏所有方法的,所以纔可以將其強轉成Map類型並調用entrySet方法

之前我們也說了動態代理的一大特點就是不論你調用代理對象的哪一個方法其實執行的都是我們創建代理對象時所傳入的InvocationHandler對象中我們所重寫的Invoke方法。這裏傳入的InvocationHandler對象就是我們之前實例化的ConvertedClosure我們看一下該類的繼承關係

可以看到ConvertedClosure類的繼承關係中其父類ConversionHandler實現了InvocationHandler並重寫了Invoke方法,所以我們由此可知當我們調用代理對象map.entrySet方法時實際上執行的是ConversionHandler.Invoke方法。我們跟進方法繼續分析。

緊接着由調用了invokeCustom方法,該方法在ConversionHandler中是一個抽象方法,所以調用的是其子類重寫的ConvertedClosure.invokeCustom方法。

之前我們創建ConvertedClosure對象時爲methodName屬性賦了值“entrySet”此時我們調用的是代理對象的entrySet方法,自然傳遞進來method的值也是“entrySet”符合判斷。

接下來的getDelegate()是其父類的方法也就是ConversionHandler.getDelegate()

返回一個MethodClosure對象也就是並將其強轉成Closure,然後調用Closure.call()方法

緊接着調用Closure的父類GroovyObjectSupport.getMetaClass()方法返回一個MetaClassImpl對象並調用MetaClassImpl.invokeMethod()方法

步入跟進該方法

MetaMethod method = null;
......
   if (method==null) {
            method = getMethodWithCaching(sender, methodName, arguments, isCallToSuper);
        }
......
    final boolean isClosure = object instanceof Closure;
        if (isClosure) {
            final Closure closure = (Closure) object;

            final Object owner = closure.getOwner();

            if (CLOSURE_CALL_METHOD.equals(methodName) || CLOSURE_DO_CALL_METHOD.equals(methodName)) {
                final Class objectClass = object.getClass();
                if (objectClass == MethodClosure.class) {
                    final MethodClosure mc = (MethodClosure) object;
                    methodName = mc.getMethod();
                    final Class ownerClass = owner instanceof Class ? (Class) owner : owner.getClass();
                    final MetaClass ownerMetaClass = registry.getMetaClass(ownerClass);
                    return ownerMetaClass.invokeMethod(ownerClass, owner, methodName, arguments, false, false);

該方法代碼過多先截取關鍵代碼,首先創建一個Method類型的變量併爲其賦值,然後我們通過判斷傳入的Object是否是Closure的子類,由截圖可以看出Object裏存儲的是一個MethodClosure對象,所以判斷的結果是true 接下來就走第一條判斷成功執行的代碼。

接下來執行的就是將Object強轉爲Closure類型,接下來取出我們一開始我們在創建MethodClosure對象時存入的要執行的命令。

接下來就一路執行到return ownerMetaClass.invokeMethod()

我們看到這個ownerMetaClass其實還是一個MetaClassImpl對象也就是說這裏其實是一個遞歸調用。

以下是遞歸調用的執行路徑可以看到在if (isClosure)這裏判斷失敗了,所以不再執行剛纔的代碼改爲執行method.doMethodInvoke()

MetaMethod method = null;
......
if (method == null)
     method = tryListParamMetaMethod(sender, methodName, isCallToSuper, arguments);
......
final boolean isClosure = object instanceof Closure;
if (isClosure) {
  ......
}
if (method != null) {
      return method.doMethodInvoke(object, arguments);
   } 
......

我們看到method變量裏存儲的是一個叫dgm的對象

以下是傳入method.doMethodInvoke() 的兩個參數裏面所存儲的值

我們要執行的命令被傳進了ProcessGroovyMethods.execute((String)var1)方法中,繼續跟進。

至此通過調用Map.entrySet()方法就能導致代碼執行的原理水落石出。

以上就是ysoserial的payload中的Groovy的gadget介紹。接下來要講的就是反序列化漏洞中的反序列化如何配和Groovy1的gadget來遠程代碼執行的。

我們來看ysoserial Groovy1所執行的全部代碼。我們可以看到在第34行代碼以前,執行的代碼和我們之前看到的簡化版的代碼執行Demo是一樣的。

我們看到我們通過反射先是拿到了AnnotationInvocationHandler此類的Class對象,然後在通過該Class對象以反射的形式拿到了它的構造方法,並最終通過該構造方法反射並傳入兩個參數一個是Override.class一個常見的註解類對象。而另一個就是我們之前所分析的可以通過調用Map.entrySet()方法可以造成代碼執行的Map對象。

爲什麼我們要如此的費力通過反射形式來生成一個AnnotationInvocationHandler對象呢?由以下截圖可知。因爲該類的構造方法和該類本身都不是public修飾的,所以我們沒法通過new一個對象的形式來創建AnnotationInvocationHandler對象

之前已經簡單介紹過了什麼是反序列化,JDK序列化/反序列化。如果反序列化的類裏有readObject方法,那麼就一定會調用該方法。這就給了我們一個可趁之機,我們觀察一下AnnotationInvocationHandler對象中都執行了些什麼。

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
    annotationType = AnnotationType.getInstance(type);
} catch (IllegalArgumentException e) {
    // Class is no longer an annotation type; time to punch out
    throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    String name = memberValue.getKey();
    Class<?> memberType = memberTypes.get(name);
    if (memberType != null) {  // i.e. member still exists
        Object value = memberValue.getValue();
        if (!(memberType.isInstance(value) ||
                value instanceof ExceptionProxy)) {
            memberValue.setValue(
                    new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                            annotationType.members().get(name)));
        }
    }
}
}

我們在這段代碼裏看到了一個熟悉的影子,在readObject方法裏有一個foreach循環,裏面有一個名字叫memberValues的變量調用的entrySet(),也就是說,如果這個memberValues裏面存儲的是我們之前構造好的那個實現了Map接口的代理對象的話,那就意味着這裏就像一個炸彈的引爆點一樣,會瞬間執行我們剛纔所分析的代碼執行路徑,並最終執行我們提前包裝好的代碼。

好,那接下來的問題是這個變量我們可以控制麼?如果該變量不接受外部傳入的參數那麼這個點就變的毫無價值。但是我們通過分析驚喜的發現,memberValues是一個全局變量,接受的恰好就是我們精心構造的那個可以執行代碼的代理對象。

AnnotationInvocationHandler對我們來說就是一個反序列化的入口點,就像是一個引爆器一樣。而我們封裝好的那個代理對象就是炸彈,在AnnotationInvocationHandler進行序列化時被封裝了進去作爲AnnotationInvocationHandler對象一個被序列化的屬性存在着,等到AnnotationInvocationHandler對象被反序列化時,就瞬間爆炸,一系列的嵌套調用瞬間到達執行Runtime.getRuntime().exec()的位置

我們以上所介紹的AnnotationInvocationHandler是低版本的JDK中的,我所使用的是1.7.0_21這個版本來做的演示,但是經過驗證高版本的JDK中的AnnotationInvocationHandler這個類雖然經過了修改但是仍然存在這個問題,還是可以觸發我們的gadget以下是JDK1.8.0._211版本的AnnotationInvocationHandler類的readObject方法的源碼

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }

可以看到出發點仍然在foreach循環的條件裏面,只不過是獲取構造好的動態代理對象的方式發生了一點變化,但是仍然不會影響我們使用。

至此ysoserial Java 反序列化系列第一集 Groovy1原理分析結束

 

總結

其實網上反序列化的文章有很多,但是不知爲何大家講解反序列化漏洞時都是用CC鏈也就是Apache.CommonsCollections來進行舉例,平心而論筆者覺得這個利用鏈一開始沒接觸過反序列化的同學直接理解還有一定的難度的,難在整個CC鏈的調用看上去略微複雜,並不是難在反序列化的部分。所筆者挑了一個個人覺得調用鏈比較清晰明瞭的Groovy來進行java 反序列化分析的第一篇文章,來幫助大家能更快速的瞭解java 反序列化漏洞。雖然Groovy1這個gadget在實際生產環境中碰的的概率可能少之又少,但是作爲一個反序列化入門學習的例子筆者個人覺得還是比較適合的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章