Java 反序列化工具 gadgetinspector 初窺

做者:Longofo@知道創宇404實驗室 
時間:2019年9月4日html

原由

一開始是聽@Badcode師傅說的這個工具,在Black Hat 2018的一個議題提出來的。這是一個基於字節碼靜態分析的、利用已知技巧自動查找從source到sink的反序列化利用鏈工具。看了幾遍做者在Black Hat上的演講視頻PPT,想從做者的演講與PPT中獲取更多關於這個工具的原理性的東西,但是有些地方真的很費解。不過做者開源了這個工具,但沒有給出詳細的說明文檔,對這個工具的分析文章也不多,看到一篇平安集團對這個工具的分析,從文中描述來看,他們對這個工具應該有必定的認識並作了一些改進,可是在文章中對某些細節沒有作過多的闡釋。後面嘗試了調試這個工具,大體理清了這個工具的工做原理,下面是對這個工具的分析過程,以及對將來工做與改進的設想。java

關於這個工具

  • 這個工具不是用來尋找漏洞,而是利用已知的source->...->sink鏈或其類似特徵發現分支利用鏈或新的利用鏈。node

  • 這個工具是在整個應用的classpath中尋找利用鏈。python

  • 這個工具進行了一些合理的預估風險判斷(污點判斷、污點傳遞等)。git

  • 這個工具會產生誤報不是漏報(其實這裏仍是會漏報,這是做者使用的策略決定的,在後面的分析中能夠看到)。github

  • 這個工具是基於字節碼分析的,對於Java應用來講,不少時候咱們並無源碼,而只有War包、Jar包或class文件。json

  • 這個工具不會生成能直接利用的Payload,具體的利用構造還須要人工參與。windows

序列化與反序列化

序列化(Serialization)是將對象的狀態信息轉化爲能夠存儲或者傳輸形式的過程,轉化後的信息能夠存儲在磁盤上,在網絡傳輸過程當中,能夠是字節、XML、JSON等格式;而將字節、XML、JSON等格式的信息還原成對象這個相反的過程稱爲反序列化。緩存

在JAVA中,對象的序列化和反序列化被普遍的應用到RMI(遠程方法調用)及網絡傳輸中。網絡

Java中的序列化與反序列化庫

  • JDK(ObjectInputStream)

  • XStream(XML,JSON)

  • Jackson(XML,JSON)

  • Genson(JSON)

  • JSON-IO(JSON)

  • FlexSON(JSON)

  • Fastjson(JSON)

  • ...

不一樣的反序列化庫在反序列化不一樣的類時有不一樣的行爲、被反序列化類的不一樣"魔術方法"會被自動調用,這些被自動調用的方法就可以做爲反序列化的入口點(source)。若是這些被自動調用的方法又調用了其餘子方法,那麼在調用鏈中某一個子方法也能夠做爲source,就至關於已知了調用鏈的前部分,從某個子方法開始尋找不一樣的分支。經過方法的層層調用,可能到達某些危險的方法(sink)。

  • ObjectInputStream

例如某個類實現了Serializable接口,ObjectInputStream.readobject在反序列化類獲得其對象時會自動查找這個類的readObject、readResolve等方法並調用。

例如某個類實現了Externalizable接口,ObjectInputStream.readobject在反序列化類獲得其對象時會自動查找這個類的readExternal等方法並調用。

  • Jackson

ObjectMapper.readValue在反序列化類獲得其對象時,會自動查找反序列化類的無參構造方法、包含一個基礎類型參數的構造方法、屬性的setter、屬性的getter等方法並調用。

  • ...

在後面的分析中,都使用JDK自帶的ObjectInputStream做爲樣例。

控制數據類型=>控制代碼

做者說,在反序列化漏洞中,若是控制了數據類型,咱們就控制了代碼。這是什麼意思呢?按個人理解,寫了下面的一個例子:

public class TestDeserialization {

    interface Animal {
        public void eat();
    }

    public static class Cat implements Animal,Serializable {
        @Override        public void eat() {
            System.out.println("cat eat fish");
        }
    }

    public static class Dog implements Animal,Serializable {
        @Override        public void eat() {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("dog eat bone");
        }
    }

    public static class Person implements Serializable {
        private Animal pet;

        public Person(Animal pet){
            this.pet = pet;
        }

        private void readObject(java.io.ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            pet = (Animal) stream.readObject();
            pet.eat();
        }
    }

    public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //將構造好的payload序列化後寫入文件中        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
        out.flush();
        out.close();
    }

    public static void payloadTest(String file) throws Exception {
        //讀取寫入的payload,並進行反序列化        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }

    public static void main(String[] args) throws Exception {
        Animal animal = new Dog();
        Person person = new Person(animal);
        GeneratePayload(person,"test.ser");
        payloadTest("test.ser");//        Animal animal = new Cat();//        Person person = new Person(animal);//        GeneratePayload(person,"test.ser");//        payloadTest("test.ser");    }}

爲了方便我把全部類寫在一個類中進行測試。在Person類中,有一個Animal類的屬性pet,它是Cat和Dog的接口。在序列化時,咱們可以控制Person的pet具體是Cat對象或者Dog對象,所以在反序列化時,在readObject中pet.eat()具體的走向就不同了。若是是pet是Cat類對象,就不會走到執行有害代碼Runtime.getRuntime().exec("calc");這一步,可是若是pet是Dog類的對象,就會走到有害代碼。

即便有時候類屬性在聲明時已經爲它賦值了某個具體的對象,可是在Java中經過反射等方式依然能修改。以下:

public class TestDeserialization {

    interface Animal {
        public void eat();
    }

    public static class Cat implements Animal, Serializable {
        @Override
        public void eat() {
            System.out.println("cat eat fish");
        }                           
    }

    public static class Dog implements Animal, Serializable {
        @Override
        public void eat() {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("dog eat bone");
        }
    }

    public static class Person implements Serializable {
        private Animal pet = new Cat();

        private void readObject(java.io.ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            pet = (Animal) stream.readObject();
            pet.eat();
        }
    }

    public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //將構造好的payload序列化後寫入文件中
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
        out.flush();
        out.close();
    }

    public static void payloadTest(String file) throws Exception {
        //讀取寫入的payload,並進行反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }

    public static void main(String[] args) throws Exception {
        Animal animal = new Dog();
        Person person = new Person();

        //經過反射修改私有屬性
        Field field = person.getClass().getDeclaredField("pet");
        field.setAccessible(true);
        field.set(person, animal);

        GeneratePayload(person, "test.ser");
        payloadTest("test.ser");
    }
}

在Person類中,不能經過構造器或setter方法或其餘方式對pet賦值,屬性在聲明時已經被定義爲Cat類的對象,可是經過反射能將pet修改成Dog類的對象,所以在反序列化時依然會走到有害代碼處。

這只是我本身對做者"控制了數據類型,就控制了代碼"的理解,在Java反序列化漏洞中,不少時候是利用到了Java的多態特性來控制代碼走向最後達到惡意執行目的。

魔術方法

在上面的例子中,能看到在反序列化時沒有調用Person的readobject方法,它是ObjectInputStream在反序列化對象時自動調用的。做者將在反序列化中會自動調用的方法稱爲"魔術方法"。

使用ObjectInputStream反序列化時幾個常見的魔術方法:

  • Object.readObject()

  • Object.readResolve()

  • Object.finalize()

  • ...

一些可序列化的JDK類實現了上面這些方法而且還自動調用了其餘方法(能夠做爲已知的入口點):

  • HashMap

    • Object.hashCode()

    • Object.equals()

  • PriorityQueue

    • Comparator.compare()

    • Comparable.CompareTo()

  • ...

一些sink:

  • Runtime.exec(),這種最爲簡單直接,即直接在目標環境中執行命令

  • Method.invoke(),這種須要適當地選擇方法和參數,經過反射執行Java方法

  • RMI/JNDI/JRMP等,經過引用遠程對象,間接實現任意代碼執行的效果

  • ...

做者給出了一個從Magic Methods(source)->Gadget Chains->Runtime.exec(sink)的例子:

70.png

上面的HashMap實現了readObject這個"魔術方法",而且調用了hashCode方法。某些類爲了比較對象之間是否相等會實現equals方法(通常是equals和hashCode方法同時實現)。從圖中能夠看到AbstractTableModel$ff19274a正好實現了hashCode方法,其中又調用了f.invoke方法,f是IFn對象,而且f能經過屬性__clojureFnMap獲取到。IFn是一個接口,上面說到,若是控制了數據類型,就控制了代碼走向。因此若是咱們在序列化時,在__clojureFnMap放置IFn接口的實現類FnCompose的一個對象,那麼就能控制f.invokeFnCompose.invoke方法,接着控制FnCompose.invoke中的f一、f2爲FnConstant就能到達FnEval.invoke了(關於AbstractTableModel$ff19274a.hashcode中的f.invoke具體選擇IFn的哪一個實現類,根據後面對這個工具的測試以及對決策原理的分析,廣度優先會選擇短的路徑,也就是選擇了FnEval.invoke,因此這也是爲何要人爲參與,在後面的樣例分析中也能夠看到)。

有了這條鏈,只須要找到觸發這個鏈的漏洞點就好了。Payload使用JSON格式表示以下:

{
    "@class":"java.util.HashMap",
    "members":[
        2,
        {
            "@class":"AbstractTableModel$ff19274a",
            "__clojureFnMap":{
                "hashcode":{
                    "@class":"FnCompose",
                    "f1":{"@class","FnConstant",value:"calc"},
                    "f2":{"@class":"FnEval"}
                }
            }
        }
    ]
}

gadgetinspector工做流程

如做者所說,正好使用了五個步驟:

        // 枚舉所有類以及類的全部方法        if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
                || !Files.exists(Paths.get("inheritanceMap.dat"))) {
            LOGGER.info("Running method discovery...");
            MethodDiscovery methodDiscovery = new MethodDiscovery();
            methodDiscovery.discover(cla***esourceEnumerator);
            methodDiscovery.save();
        }
        //生成passthrough數據流        if (!Files.exists(Paths.get("passthrough.dat"))) {
            LOGGER.info("Analyzing methods for passthrough dataflow...");
            PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();
            passthroughDiscovery.discover(cla***esourceEnumerator, config);
            passthroughDiscovery.save();
        }
        //生成passthrough調用圖        if (!Files.exists(Paths.get("callgraph.dat"))) {
            LOGGER.info("Analyzing methods in order to build a call graph...");
            CallGraphDiscovery callGraphDiscovery = new CallGraphDiscovery();
            callGraphDiscovery.discover(cla***esourceEnumerator, config);
            callGraphDiscovery.save();
        }
        //搜索可用的source        if (!Files.exists(Paths.get("sources.dat"))) {
            LOGGER.info("Discovering gadget chain source methods...");
            SourceDiscovery sourceDiscovery = config.getSourceDiscovery();
            sourceDiscovery.discover();
            sourceDiscovery.save();
        }
        //搜索生成調用鏈        {
            LOGGER.info("Searching call graph for gadget chains...");
            GadgetChainDiscovery gadgetChainDiscovery = new GadgetChainDiscovery(config);
            gadgetChainDiscovery.discover();
        }

Step1 枚舉所有類以及每一個類的全部方法

要進行調用鏈的搜索,首先得有全部類及全部類方法的相關信息:

public class MethodDiscovery {

    private static final Logger LOGGER = LoggerFactory.getLogger(MethodDiscovery.class);

    private final List<Cla***eference> discoveredClasses = new ArrayList<>();//保存全部類信息    private final List<MethodReference> discoveredMethods = new ArrayList<>();//保存全部方法信息    ...
    ...
    public void discover(final Cla***esourceEnumerator cla***esourceEnumerator) throws Exception {
        //cla***esourceEnumerator.getAllClasses()獲取了運行時的全部類(JDK rt.jar)以及要搜索應用中的全部類        for (Cla***esourceEnumerator.Cla***esource cla***esource : cla***esourceEnumerator.getAllClasses()) {
            try (InputStream in = cla***esource.getInputStream()) {
                Cla***eader cr = new Cla***eader(in);
                try {
                    cr.accept(new MethodDiscoveryClassVisitor(), Cla***eader.EXPAND_FRAMES);//經過ASM框架操做字節碼並將類信息保存到this.discoveredClasses,將方法信息保存到discoveredMethods                } catch (Exception e) {
                    LOGGER.error("Exception analyzing: " + cla***esource.getName(), e);
                }
            }
        }
    }
    ...
    ...
    public void save() throws IOException {
        DataLoader.saveData(Paths.get("classes.dat"), new Cla***eference.Factory(), discoveredClasses);//將類信息保存到classes.dat        DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);//將方法信息保存到methods.dat
        Map<Cla***eference.Handle, Cla***eference> classMap = new HashMap<>();
        for (Cla***eference clazz : discoveredClasses) {
            classMap.put(clazz.getHandle(), clazz);
        }
        InheritanceDeriver.derive(classMap).save();//查找全部繼承關係並保存    }}

來看下classes.dat、methods.dat分別長什麼樣子:

  • classes.dat

找了兩個比較有特徵的

類名 父類名 全部接口 是不是接口 成員
com/sun/deploy/jardiff/JarDiffPatcher java/lang/Object com/sun/deploy/jardiff/JarDiffConstants,com/sun/deploy/jardiff/Patcher false newBytes!2![B
com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl com/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializable false stub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl

第一個類com/sun/deploy/jardiff/JarDiffPatcher:

71.png

和上面的表格信息對應一下,是吻合的

  • 類名:com/sun/deploy/jardiff/JarDiffPatcher

  • 父類: java/lang/Object,若是一類沒有顯式繼承其餘類,默認隱式繼承java/lang/Object,而且java中不容許多繼承,因此每一個類只有一個父類

  • 全部接口:com/sun/deploy/jardiff/JarDiffConstants、com/sun/deploy/jardiff/Patcher

  • 是不是接口:false

  • 成員:newBytes!2![B,newBytes成員,Byte類型。爲何沒有將static/final類型的成員加進去呢?這裏尚未研究如何操做字節碼,因此做者這裏的判斷實現部分暫且跳過。不過猜想應該是這種類型的變量並不能成爲污點因此忽略了

第二個類com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl:

72.png

和上面的表格信息對應一下,也是吻合的

  • 類名:com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl,是一個內部類

  • 父類: com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl

  • 全部接口:com/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializable

  • 是不是接口:false

  • 成員:stub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl,!*!這裏能夠暫時理解爲分割符,有一個成員stub,類型com/sun/corba/se/spi/presentation/rmi/DynamicStub。由於是內部類,因此多了個this成員,這個this指向的是外部類

  • methods.dat

一樣找幾個比較有特徵的

類名 方法名 方法描述信息 是不是靜態方法
sun/nio/cs/ext/Big5 newEncoder ()Ljava/nio/charset/CharsetEncoder; false
sun/nio/cs/ext/Big5_HKSCS$Decoder \<init> (Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS$1;)V false

sun/nio/cs/ext/Big5#newEncoder:

  • 類名:sun/nio/cs/ext/Big5

  • 方法名: newEncoder

  • 方法描述信息: ()Ljava/nio/charset/CharsetEncoder; 無參,返回java/nio/charset/CharsetEncoder對象

  • 是不是靜態方法:false

sun/nio/cs/ext/Big5_HKSCS$Decoder#\<init>:

  • 類名:sun/nio/cs/ext/Big5_HKSCS$Decoder

  • 方法名:\<init>

  • 方法描述信息: (Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS1;)V1java/nio/charset/Charset2sun/nio/cs/ext/Big5HKSCS1;)V參數1是java/nio/charset/Charset類型,參數2是sun/nio/cs/ext/Big5HKSCS1類型,返回值void

  • 是不是靜態方法:false

繼承關係的生成:

繼承關係在後面用來判斷一個類是否能被某個庫序列化、以及搜索子類方法實現等會用到。

public class InheritanceDeriver {
    private static final Logger LOGGER = LoggerFactory.getLogger(InheritanceDeriver.class);

    public static InheritanceMap derive(Map<Cla***eference.Handle, Cla***eference> classMap) {
        LOGGER.debug("Calculating inheritance for " + (classMap.size()) + " classes...");
        Map<Cla***eference.Handle, Set<Cla***eference.Handle>> implicitInheritance = new HashMap<>();
        for (Cla***eference cla***eference : classMap.values()) {
            if (implicitInheritance.containsKey(cla***eference.getHandle())) {
                throw new IllegalStateException("Already derived implicit classes for " + cla***eference.getName());
            }
            Set<Cla***eference.Handle> allParents = new HashSet<>();

            getAllParents(cla***eference, classMap, allParents);//獲取當前類的全部父類
            implicitInheritance.put(cla***eference.getHandle(), allParents);
        }
        return new InheritanceMap(implicitInheritance);
    }
    ...
    ...
    private static void getAllParents(Cla***eference cla***eference, Map<Cla***eference.Handle, Cla***eference> classMap, Set<Cla***eference.Handle> allParents) {
        Set<Cla***eference.Handle> parents = new HashSet<>();
        if (cla***eference.getSuperClass() != null) {
            parents.add(new Cla***eference.Handle(cla***eference.getSuperClass()));//父類        }
        for (String iface : cla***eference.getInterfaces()) {
            parents.add(new Cla***eference.Handle(iface));//接口類        }

        for (Cla***eference.Handle immediateParent : parents) {
            //獲取間接父類,以及遞歸獲取間接父類的父類            Cla***eference parentCla***eference = classMap.get(immediateParent);
            if (parentCla***eference == null) {
                LOGGER.debug("No class id for " + immediateParent.getName());
                continue;
            }
            allParents.add(parentCla***eference.getHandle());
            getAllParents(parentCla***eference, classMap, allParents);
        }
    }
    ...
    ...}

這一步的結果保存到了inheritanceMap.dat:

直接父類+間接父類
com/sun/javaws/OperaPreferencesPreferenceSectionPreferenceSectionPreferenceEntryIterator java/lang/Object、java/util/Iterator
com/sun/java/swing/plaf/windows/WindowsLookAndFeel$XPValue java/lang/Object、javax/swing/UIDefaults$ActiveValue

Step2 生成passthrough數據流

這裏的passthrough數據流指的是每一個方法的返回結果與方法參數的關係,這一步生成的數據會在生成passthrough調用圖時用到。

以做者給出的demo爲例,先從宏觀層面判斷下:

73.png

FnConstant.invoke返回值與參數this(參數0,由於序列化時類的全部成員咱們都能控制,因此全部成員變量都視爲0參)、arg(參數1)的關係:

  • 與this的關係:返回了this.value,即與0參有關係

  • 與arg的關係:返回值與arg沒有任何關係,即與1參沒有關係

  • 結論就是FnConstant.invoke與參數0有關,表示爲FnConstant.invoke()->0

Fndefault.invoke返回值與參數this(參數0)、arg(參數1)的關係:

  • 與this的關係:返回條件的第二個分支與this.f有關係,即與0參有關係

  • 與arg的關係:返回條件的第一個分支與arg有關係,即與1參有關係

  • 結論就是FnConstant.invoke與0參,1參都有關係,表示爲Fndefault.invoke()->0、Fndefault.invoke()->1

在這一步中,gadgetinspector是利用ASM來進行方法字節碼的分析,主要邏輯是在類PassthroughDiscovery和TaintTrackingMethodVisitor中。特別是TaintTrackingMethodVisitor,它經過標記追蹤JVM虛擬機在執行方法時的stack和localvar,並最終獲得返回結果是否能夠被參數標記污染。

核心實現代碼(TaintTrackingMethodVisitor涉及到字節碼分析,暫時先不看):

public class PassthroughDiscovery {

    private static final Logger LOGGER = LoggerFactory.getLogger(PassthroughDiscovery.class);

    private final Map<MethodReference.Handle, Set<MethodReference.Handle>> methodCalls = new HashMap<>();
    private Map<MethodReference.Handle, Set<Integer>> passthroughDataflow;

    public void discover(final Cla***esourceEnumerator cla***esourceEnumerator, final GIConfig config) throws IOException {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();//load以前保存的methods.dat        Map<Cla***eference.Handle, Cla***eference> classMap = DataLoader.loadClasses();//load以前保存的classes.dat        InheritanceMap inheritanceMap = InheritanceMap.load();//load以前保存的inheritanceMap.dat
        Map<String, Cla***esourceEnumerator.Cla***esource> cla***esourceByName = discoverMethodCalls(cla***esourceEnumerator);//查找一個方法中包含的子方法        List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();//對全部方法構成的圖執行逆拓撲排序        passthroughDataflow = calculatePassthroughDataflow(cla***esourceByName, classMap, inheritanceMap, sortedMethods,
                config.getSerializableDecider(methodMap, inheritanceMap));//計算生成passthrough數據流,涉及到字節碼分析    }
    ...
    ...
    private List<MethodReference.Handle> topologicallySortMethodCalls() {
        Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>();
        for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodCalls.entrySet()) {
            MethodReference.Handle method = entry.getKey();
            outgoingReferences.put(method, new HashSet<>(entry.getValue()));
        }

        // 對全部方法構成的圖執行逆拓撲排序        LOGGER.debug("Performing topological sort...");
        Set<MethodReference.Handle> dfsStack = new HashSet<>();
        Set<MethodReference.Handle> visitedNodes = new HashSet<>();
        List<MethodReference.Handle> sortedMethods = new ArrayList<>(outgoingReferences.size());
        for (MethodReference.Handle root : outgoingReferences.keySet()) {
            dfsTsort(outgoingReferences, sortedMethods, visitedNodes, dfsStack, root);
        }
        LOGGER.debug(String.format("Outgoing references %d, sortedMethods %d", outgoingReferences.size(), sortedMethods.size()));

        return sortedMethods;
    }
    ...
    ...
    private static void dfsTsort(Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences,
                                    List<MethodReference.Handle> sortedMethods, Set<MethodReference.Handle> visitedNodes,
                                    Set<MethodReference.Handle> stack, MethodReference.Handle node) {

        if (stack.contains(node)) {//防止在dfs一條方法調用鏈中進入循環            return;
        }
        if (visitedNodes.contains(node)) {//防止對某個方法及子方法重複排序            return;
        }
        Set<MethodReference.Handle> outgoingRefs = outgoingReferences.get(node);
        if (outgoingRefs == null) {
            return;
        }

        stack.add(node);
        for (MethodReference.Handle child : outgoingRefs) {
            dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, child);
        }
        stack.remove(node);
        visitedNodes.add(node);
        sortedMethods.add(node);
    }}

拓撲排序

有向無環圖(DAG)纔有拓撲排序,非 DAG 圖沒有拓撲排序。 當有向無環圖知足如下條件時:

  • 每個頂點出現且只出現一次

  • 若A在序列中排在B的前面,則在圖中不存在從B到A的路徑

74.png

這樣的圖,是一個拓撲排序的圖。樹結構其實能夠轉化爲拓撲排序,而拓撲排序 不必定可以轉化爲樹。

以上面的拓撲排序圖爲例,用一個字典表示圖結構

 graph = {
     "a": ["b","d"],
     "b": ["c"],
     "d": ["e","c"],
     "e": ["c"],
     "c": [],
 }

代碼實現

graph = {
    "a": ["b","d"],
    "b": ["c"],
    "d": ["e","c"],
    "e": ["c"],
    "c": [],}def TopologicalSort(graph):
  degrees = dict((u, 0) for u in graph)
  for u in graph:
      for v in graph[u]:
          degrees[v] += 1
  #入度爲0的插入隊列  queue = [u for u in graph if degrees[u] == 0]
  res = []
  while queue:
      u = queue.pop()
      res.append(u)
      for v in graph[u]:
          # 移除邊,即將當前元素相關元素的入度-1          degrees[v] -= 1
          if degrees[v] == 0:
              queue.append(v)
  return resprint(TopologicalSort(graph)) # ['a', 'd', 'e', 'b', 'c']

可是在方法的調用中,咱們但願最後的結果是c、b、e、d、a,這一步須要逆拓撲排序,正向排序使用的BFS,那麼獲得相反結果可使用DFS。爲何在方法調用中須要使用逆拓撲排序呢,這與生成passthrough數據流有關。看下面一個例子:

...
    public String parentMethod(String arg){
        String vul = Obj.childMethod(arg);
        return vul;
    }...

那麼這裏arg與返回值到底有沒有關係呢?假設Obj.childMethod爲

...
    public String childMethod(String carg){
        return carg.toString();
    }...

因爲childMethod的返回值carg與有關,那麼能夠斷定parentMethod的返回值與參數arg是有關係的。因此若是存在子方法調用並傳遞了父方法參數給子方法時,須要先判斷子方法返回值與子方法參數的關係。所以須要讓子方法的判斷在前面,這就是爲何要進行逆拓撲排序。

從下圖能夠看出outgoingReferences的數據結構爲:

{
    method1:(method2,method3,method4),

    method5:(method1,method6),
    ...}

而這個結構正好適合逆拓撲排序

75.png

可是上面說拓撲排序時不能造成環,可是在方法調用中確定是會存在環的。做者是如何避免的呢?

在上面的dfsTsort實現代碼中能夠看到使用了stack和visitedNodes,stack保證了在進行逆拓撲排序時不會造成環,visitedNodes避免了重複排序。使用以下一個調用圖來演示過程:

76.png

從圖中能夠看到有環med1->med2->med6->med1,而且有重複的調用med3,嚴格來講並不能進行逆拓撲排序,可是經過stack、visited記錄訪問過的方法,就能實現逆拓撲排序。爲了方便解釋把上面的圖用一個樹來表示:

77.png

對上圖進行逆拓撲排序(DFS方式):

從med1開始,先將med1加入stack中,此時stack、visited、sortedmethods狀態以下:

78.png

med1還有子方法?有,繼續深度遍歷。將med2放入stack,此時的狀態:

79.png

med2有子方法嗎?有,繼續深度遍歷。將med3放入stack,此時的狀態:

80.png

med3有子方法嗎?有,繼續深度遍歷。將med7放入stack,此時的狀態:

81.png

med7有子方法嗎?沒有,從stack中彈出med7並加入visited和sortedmethods,此時的狀態:

82.png

回溯到上一層,med3還有其餘子方法嗎?有,med8,將med8放入stack,此時的狀態:

83.png

med8還有子方法嗎?沒有,彈出stack,加入visited與sortedmethods,此時的狀態:

84.png

回溯到上一層,med3還有其餘子方法嗎?沒有了,彈出stack,加入visited與sortedmethods,此時的狀態:

85.png

回溯到上一層,med2還有其餘子方法嗎?有,med6,將med6加入stack,此時的狀態:

86.png

med6還有子方法嗎?有,med1,med1在stack中?不加入,拋棄。此時狀態和上一步同樣

回溯到上一層,med6還有其餘子方法嗎?沒有了,彈出stack,加入visited和sortedmethods,此時的狀態:

87.png

回溯到上一層,med2還有其餘子方法嗎?沒有了,彈出stack,加入visited和sortedmethods,此時的狀態:

88.png

回溯到上一層,med1還有其餘子方法嗎?有,med3,med3在visited中?在,拋棄。

回溯到上一層,med1還有其餘子方法嗎?有,med4,將med4加入stack,此時的狀態:

89.png

med4還有其餘子方法嗎?沒有,彈出stack,加入visited和sortedmethods中,此時的狀態:

90.png

回溯到上一層,med1還有其餘子方法嗎?沒有了,彈出stack,加入visited和sortedmethods中,此時的狀態(即最終狀態):

91.png

因此最後的逆拓撲排序結果爲:med七、med八、med三、med六、med二、med四、med1。

生成passthrough數據流

在calculatePassthroughDataflow中遍歷了sortedmethods,並經過字節碼分析,生成了方法返回值與參數關係的passthrough數據流。注意到下面的序列化決定器,做者內置了三種:JDK、Jackson、Xstream,會根據具體的序列化決定器斷定決策過程當中的類是否符合對應庫的反序列化要求,不符合的就跳過:

  • 對於JDK(ObjectInputStream),類否繼承了Serializable接口

  • 對於Jackson,類是否存在0參構造器

  • 對於Xstream,類名可否做爲有效的XML標籤

生成passthrough數據流代碼:

...
    private static Map<MethodReference.Handle, Set<Integer>> calculatePassthroughDataflow(Map<String, Cla***esourceEnumerator.Cla***esource> cla***esourceByName,
                                                                                          Map<Cla***eference.Handle, Cla***eference> classMap,
                                                                                          InheritanceMap inheritanceMap,
                                                                                          List<MethodReference.Handle> sortedMethods,
                                                                                          SerializableDecider serializableDecider) throws IOException {
        final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>();
        for (MethodReference.Handle method : sortedMethods) {//依次遍歷sortedmethods,而且每一個方法的子方法斷定總在這個方法以前,這是經過的上面的逆拓撲排序實現的。            if (method.getName().equals("<clinit>")) {
                continue;
            }
            Cla***esourceEnumerator.Cla***esource cla***esource = cla***esourceByName.get(method.getCla***eference().getName());
            try (InputStream inputStream = cla***esource.getInputStream()) {
                Cla***eader cr = new Cla***eader(inputStream);
                try {
                    PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap,
                            passthroughDataflow, serializableDecider, Opcodes.ASM6, method);
                    cr.accept(cv, Cla***eader.EXPAND_FRAMES);//經過結合classMap、inheritanceMap、已斷定出的passthroughDataflow結果、序列化決定器信息來斷定當前method的返回值與參數的關係                    passthroughDataflow.put(method, cv.getReturnTaint());//將斷定後的method與有關係的污染點加入passthroughDataflow                } catch (Exception e) {
                    LOGGER.error("Exception analyzing " + method.getCla***eference().getName(), e);
                }
            } catch (IOException e) {
                LOGGER.error("Unable to analyze " + method.getCla***eference().getName(), e);
            }
        }
        return passthroughDataflow;
    }...

最後生成了passthrough.dat:

類名 方法名 方法描述 污點
java/util/Collections$CheckedNavigableSet tailSet (Ljava/lang/Object;)Ljava/util/NavigableSet; 0,1
java/awt/RenderingHints put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; 0,1,2

Step3 枚舉passthrough調用圖

這一步和上一步相似,gadgetinspector 會再次掃描所有的Java方法,但檢查的再也不是參數與返回結果的關係,而是方法的參數與其所調用的子方法的關係,即子方法的參數是否能夠被父方法的參數所影響。那麼爲何要進行上一步的生成passthrough數據流呢?因爲這一步的判斷也是在字節碼分析中,因此這裏只能先進行一些猜想,以下面這個例子:

...
    private MyObject obj;

    public void parentMethod(Object arg){
        ...
        TestObject obj1 = new TestObject();
        Object obj2 = obj1.childMethod1(arg);
        this.obj.childMethod(obj2); 
        ...
    }...

若是不進行生成passthrough數據流操做,就沒法判斷TestObject.childMethod1的返回值是否會受到參數1的影響,也就沒法繼續判斷parentMethod的arg參數與子方法MyObject.childmethod的參數傳遞關係。

做者給出的例子:

92.png

AbstractTableModel$ff19274a.hashcode與子方法IFn.invoke:

  • AbstractTableModel$ff19274a.hashcode的this(0參)傳遞給了IFn.invoke的1參,表示爲0->IFn.invoke()@1

  • 因爲f是經過this.__clojureFnMap(0參)獲取的,而f又爲IFn.invoke()的this(0參),即AbstractTableModel$ff19274a.hashcode的0參傳遞給了IFn.invoke的0參,表示爲0->IFn.invoke()@0

FnCompose.invoke與子方法IFn.invoke:

  • FnCompose.invoked的arg(1參)傳遞給了IFn.invoke的1參,表示爲1->IFn.invoke()@1

  • f1爲FnCompose的屬性(this,0參),被作爲了IFn.invoke的this(0參數)傳遞,表示爲0->IFn.invoke()@1

  • f1.invoke(arg)作爲一個總體被看成1參傳遞給了IFn.invoke,因爲f1在序列化時咱們能夠控制具體是IFn的哪一個實現類,因此具體調用哪一個實現類的invoke也至關於可以控制,即f1.invoke(arg)這個總體能夠視爲0參數傳遞給了IFn.invoke的1參(這裏只是進行的簡單猜想,具體實如今字節碼分析中,可能也體現了做者說的合理的風險判斷吧),表示爲0->IFn.invoke()@1

在這一步中,gadgetinspector也是利用ASM來進行字節碼的分析,主要邏輯是在類CallGraphDiscovery和ModelGeneratorClassVisitor中。在ModelGeneratorClassVisitor中經過標記追蹤JVM虛擬機在執行方法時的stack和localvar,最終獲得方法的參數與其所調用的子方法的參數傳遞關係。

生成passthrough調用圖代碼(暫時省略ModelGeneratorClassVisitor的實現,涉及到字節碼分析):

public class CallGraphDiscovery {
    private static final Logger LOGGER = LoggerFactory.getLogger(CallGraphDiscovery.class);

    private final Set<GraphCall> discoveredCalls = new HashSet<>();

    public void discover(final Cla***esourceEnumerator cla***esourceEnumerator, GIConfig config) throws IOException {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();//加載全部方法        Map<Cla***eference.Handle, Cla***eference> classMap = DataLoader.loadClasses();//加載全部類        InheritanceMap inheritanceMap = InheritanceMap.load();//加載繼承圖        Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = PassthroughDiscovery.load();//加載passthrough數據流
        SerializableDecider serializableDecider = config.getSerializableDecider(methodMap, inheritanceMap);//序列化決定器
        for (Cla***esourceEnumerator.Cla***esource cla***esource : cla***esourceEnumerator.getAllClasses()) {
            try (InputStream in = cla***esource.getInputStream()) {
                Cla***eader cr = new Cla***eader(in);
                try {
                    cr.accept(new ModelGeneratorClassVisitor(classMap, inheritanceMap, passthroughDataflow, serializableDecider, Opcodes.ASM6),
                            Cla***eader.EXPAND_FRAMES);//經過結合classMap、inheritanceMap、passthroughDataflow結果、序列化決定器信息來斷定當前method參數與子方法傳遞調用關係                } catch (Exception e) {
                    LOGGER.error("Error analyzing: " + cla***esource.getName(), e);
                }
            }
        }
    }

最後生成了passthrough.dat:

父方法類名 父方法 父方法描述 子方法類名 子方法子 方法描述 父方法第幾參 參數對象的哪一個field被傳遞 子方法第幾參
java/io/PrintStream write (Ljava/lang/String;)V java/io/OutputStream flush ()V 0 out 0
javafx/scene/shape/Shape setSmooth (Z)V javafx/scene/shape/Shape smoothProperty ()Ljavafx/beans/property/BooleanProperty; 0
0

Step4 搜索可用的source

這一步會根據已知的反序列化漏洞的入口,檢查全部能夠被觸發的方法。例如,在利用鏈中使用代理時,任何可序列化而且是java/lang/reflect/InvocationHandler子類的invoke方法均可以視爲source。這裏還會根據具體的反序列化庫決定類是否能被序列化。

搜索可用的source:

public class SimpleSourceDiscovery extends SourceDiscovery {

    @Override    public void discover(Map<Cla***eference.Handle, Cla***eference> classMap,
                         Map<MethodReference.Handle, MethodReference> methodMap,
                         InheritanceMap inheritanceMap) {

        final SerializableDecider serializableDecider = new SimpleSerializableDecider(inheritanceMap);

        for (MethodReference.Handle method : methodMap.keySet()) {
            if (Boolean.TRUE.equals(serializableDecider.apply(method.getCla***eference()))) {
                if (method.getName().equals("finalize") && method.getDesc().equals("()V")) {
                    addDiscoveredSource(new Source(method, 0));
                }
            }
        }

        // 若是類實現了readObject,則傳入的ObjectInputStream被認爲是污染的        for (MethodReference.Handle method : methodMap.keySet()) {
            if (Boolean.TRUE.equals(serializableDecider.apply(method.getCla***eference()))) {
                if (method.getName().equals("readObject") && method.getDesc().equals("(Ljava/io/ObjectInputStream;)V")) {
                    addDiscoveredSource(new Source(method, 1));
                }
            }
        }

        // 使用代理技巧時,任何擴展了serializable and InvocationHandler的類會受到污染。        for (Cla***eference.Handle clazz : classMap.keySet()) {
            if (Boolean.TRUE.equals(serializableDecider.apply(clazz))
                    && inheritanceMap.isSubclassOf(clazz, new Cla***eference.Handle("java/lang/reflect/InvocationHandler"))) {
                MethodReference.Handle method = new MethodReference.Handle(
                        clazz, "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;");

                addDiscoveredSource(new Source(method, 0));
            }
        }

        // hashCode()或equals()是將對象放入HashMap的標準技巧的可訪問入口點        for (MethodReference.Handle method : methodMap.keySet()) {
            if (Boolean.TRUE.equals(serializableDecider.apply(method.getCla***eference()))) {
                if (method.getName().equals("hashCode") && method.getDesc().equals("()I")) {
                    addDiscoveredSource(new Source(method, 0));
                }
                if (method.getName().equals("equals") && method.getDesc().equals("(Ljava/lang/Object;)Z")) {
                    addDiscoveredSource(new Source(method, 0));
                    addDiscoveredSource(new Source(method, 1));
                }
            }
        }

        // 使用比較器代理,能夠跳轉到任何groovy Closure的call()/doCall()方法,全部的args都被污染        // https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Groovy1.java        for (MethodReference.Handle method : methodMap.keySet()) {
            if (Boolean.TRUE.equals(serializableDecider.apply(method.getCla***eference()))
                    && inheritanceMap.isSubclassOf(method.getCla***eference(), new Cla***eference.Handle("groovy/lang/Closure"))
                    && (method.getName().equals("call") || method.getName().equals("doCall"))) {

                addDiscoveredSource(new Source(method, 0));
                Type[] methodArgs = Type.getArgumentTypes(method.getDesc());
                for (int i = 0; i < methodArgs.length; i++) {
                    addDiscoveredSource(new Source(method, i + 1));
                }
            }
        }
    }...

這一步的結果會保存在文件sources.dat中:

方法 方法描述 污染參數
java/awt/color/ICC_Profile finalize ()V 0
java/lang/Enum readObject (Ljava/io/ObjectInputStream;)V 1

Step5 搜索生成調用鏈

這一步會遍歷所有的source,並在callgraph.dat中遞歸查找全部能夠繼續傳遞污點參數的子方法調用,直至遇到sink中的方法。

搜索生成調用鏈:

public class GadgetChainDiscovery {

    private static final Logger LOGGER = LoggerFactory.getLogger(GadgetChainDiscovery.class);

    private final GIConfig config;

    public GadgetChainDiscovery(GIConfig config) {
        this.config = config;
    }

    public void discover() throws Exception {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
        InheritanceMap inheritanceMap = InheritanceMap.load();
        Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = InheritanceDeriver.getAllMethodImplementations(
                inheritanceMap, methodMap);//獲得方法的全部子類方法實現(被子類重寫的方法)
        final ImplementationFinder implementationFinder = config.getImplementationFinder(
                methodMap, methodImplMap, inheritanceMap);

        //將方法的全部子類方法實現保存到methodimpl.dat        try (Writer writer = Files.newBufferedWriter(Paths.get("methodimpl.dat"))) {
            for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodImplMap.entrySet()) {
                writer.write(entry.getKey().getCla***eference().getName());
                writer.write("\t");
                writer.write(entry.getKey().getName());
                writer.write("\t");
                writer.write(entry.getKey().getDesc());
                writer.write("\n");
                for (MethodReference.Handle method : entry.getValue()) {
                    writer.write("\t");
                    writer.write(method.getCla***eference().getName());
                    writer.write("\t");
                    writer.write(method.getName());
                    writer.write("\t");
                    writer.write(method.getDesc());
                    writer.write("\n");
                }
            }
        }

        //方法調用map,key爲父方法,value爲子方法與父方法參數傳遞關係        Map<MethodReference.Handle, Set<GraphCall>> graphCallMap = new HashMap<>();
        for (GraphCall graphCall : DataLoader.loadData(Paths.get("callgraph.dat"), new GraphCall.Factory())) {
            MethodReference.Handle caller = graphCall.getCallerMethod();
            if (!graphCallMap.containsKey(caller)) {
                Set<GraphCall> graphCalls = new HashSet<>();
                graphCalls.add(graphCall);
                graphCallMap.put(caller, graphCalls);
            } else {
                graphCallMap.get(caller).add(graphCall);
            }
        }

        //exploredMethods保存在調用鏈從查找過程當中已經訪問過的方法節點,methodsToExplore保存調用鏈        Set<GadgetChainLink> exploredMethods = new HashSet<>();
        LinkedList<GadgetChain> methodsToExplore = new LinkedList<>();
        //加載全部sources,並將每一個source做爲每條鏈的第一個節點        for (Source source : DataLoader.loadData(Paths.get("sources.dat"), new Source.Factory())) {
            GadgetChainLink srcLink = new GadgetChainLink(source.getSourceMethod(), source.getTaintedArgIndex());
            if (exploredMethods.contains(srcLink)) {
                continue;
            }
            methodsToExplore.add(new GadgetChain(Arrays.asList(srcLink)));
            exploredMethods.add(srcLink);
        }

        long iteration = 0;
        Set<GadgetChain> discoveredGadgets = new HashSet<>();
        //使用廣度優先搜索全部從source到sink的調用鏈        while (methodsToExplore.size() > 0) {
            if ((iteration % 1000) == 0) {
                LOGGER.info("Iteration " + iteration + ", Search space: " + methodsToExplore.size());
            }
            iteration += 1;

            GadgetChain chain = methodsToExplore.pop();//從隊首彈出一條鏈            GadgetChainLink lastLink = chain.links.get(chain.links.size()-1);//取這條鏈最後一個節點
            Set<GraphCall> methodCalls = graphCallMap.get(lastLink.method);//獲取當前節點方法全部子方法與當前節點方法參數傳遞關係            if (methodCalls != null) {
                for (GraphCall graphCall : methodCalls) {
                    if (graphCall.getCallerArgIndex() != lastLink.taintedArgIndex) {
                        //若是當前節點方法的污染參數與當前子方法受父方法參數影響的Index不一致則跳過                        continue;
                    }

                    Set<MethodReference.Handle> allImpls = implementationFinder.getImplementations(graphCall.getTargetMethod());//獲取子方法所在類的全部子類重寫方法
                    for (MethodReference.Handle methodImpl : allImpls) {
                        GadgetChainLink newLink = new GadgetChainLink(methodImpl, graphCall.getTargetArgIndex());//新方法節點                        if (exploredMethods.contains(newLink)) {
                            //若是新方法已近被訪問過了,則跳過,這裏能減小開銷。可是這一步跳過會使其餘鏈/分支鏈通過此節點時,因爲已經此節點被訪問過了,鏈會在這裏斷掉。那麼若是這個條件去掉就能實現找到全部鏈了嗎?這裏去掉會遇到環狀問題,形成路徑無限增長...                            continue;
                        }

                        GadgetChain newChain = new GadgetChain(chain, newLink);//新節點與以前的鏈組成新鏈                        if (isSink(methodImpl, graphCall.getTargetArgIndex(), inheritanceMap)) {//若是到達了sink,則加入discoveredGadgets                            discoveredGadgets.add(newChain);
                        } else {
                            //新鏈加入隊列                            methodsToExplore.add(newChain);
                            //新節點加入已訪問集合                            exploredMethods.add(newLink);
                        }
                    }
                }
            }
        }

        //保存搜索到的利用鏈到gadget-chains.txt        try (OutputStream outputStream = Files.newOutputStream(Paths.get("gadget-chains.txt"));
             Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
            for (GadgetChain chain : discoveredGadgets) {
                printGadgetChain(writer, chain);
            }
        }

        LOGGER.info("Found {} gadget chains.", discoveredGadgets.size());
    }...

做者給出的sink方法:

private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) {
        if (method.getCla***eference().getName().equals("java/io/FileInputStream")
                && method.getName().equals("<init>")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/io/FileOutputStream")
                && method.getName().equals("<init>")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/nio/file/Files")
                && (method.getName().equals("newInputStream")
                || method.getName().equals("newOutputStream")
                || method.getName().equals("newBufferedReader")
                || method.getName().equals("newBufferedWriter"))) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/Runtime")
                && method.getName().equals("exec")) {
            return true;
        }
        /*
        if (method.getCla***eference().getName().equals("java/lang/Class")
                && method.getName().equals("forName")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/Class")
                && method.getName().equals("getMethod")) {
            return true;
        }
        */
        // If we can invoke an arbitrary method, that's probably interesting (though this doesn't assert that we        // can control its arguments). Conversely, if we can control the arguments to an invocation but not what        // method is being invoked, we don't mark that as interesting.        if (method.getCla***eference().getName().equals("java/lang/reflect/Method")
                && method.getName().equals("invoke") && argIndex == 0) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/net/URLClassLoader")
                && method.getName().equals("newInstance")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/System")
                && method.getName().equals("exit")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/Shutdown")
                && method.getName().equals("exit")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/Runtime")
                && method.getName().equals("exit")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/nio/file/Files")
                && method.getName().equals("newOutputStream")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/lang/ProcessBuilder")
                && method.getName().equals("<init>") && argIndex > 0) {
            return true;
        }
        if (inheritanceMap.isSubclassOf(method.getCla***eference(), new Cla***eference.Handle("java/lang/ClassLoader"))
                && method.getName().equals("<init>")) {
            return true;
        }
        if (method.getCla***eference().getName().equals("java/net/URL") && method.getName().equals("openStream")) {
            return true;
        }
        // Some groovy-specific sinks        if (method.getCla***eference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper")
                && method.getName().equals("invokeMethod") && argIndex == 1) {
            return true;
        }
        if (inheritanceMap.isSubclassOf(method.getCla***eference(), new Cla***eference.Handle("groovy/lang/MetaClass"))
                && Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) {
            return true;
        }
        return false;
    }

對於每一個入口節點來講,其所有子方法調用、孫子方法調用等等遞歸下去,就構成了一棵樹。以前的步驟所作的,就至關於生成了這顆樹,而這一步所作的,就是從根節點出發,找到一條通往葉子節點的道路,使得這個葉子節點正好是咱們所指望的sink方法。gadgetinspector對樹的遍歷採用的是廣度優先(BFS),並且對於已經檢查過的節點會直接跳過,這樣減小了運行開銷,避免了環路,可是丟掉了不少其餘鏈。

這個過程看起來就像下面這樣:

93.png

經過污點的傳遞,最終找到從source->sink的利用鏈

:targ表示污染參數的index,0->1這樣的表示父方法的0參傳遞給了子方法的1參

樣例分析

如今根據做者的樣例寫個具體的demo實例來測試下上面這些步驟。

demo以下:

94.png

IFn.java:
    package com.demo.ifn;

    import java.io.IOException;

    public interface IFn {
        public Object invokeCall(Object arg) throws IOException;
    }FnEval.java    package com.demo.ifn;

    import java.io.IOException;
    import java.io.Serializable;

    public class FnEval implements IFn, Serializable {
        public FnEval() {
        }

        public Object invokeCall(Object arg) throws IOException {
            return Runtime.getRuntime().exec((String) arg);
        }
    }FnConstant.java:
    package com.demo.ifn;

    import java.io.Serializable;

    public class FnConstant implements IFn , Serializable {
        private Object value;

        public FnConstant(Object value) {
            this.value = value;
        }

        public Object invokeCall(Object arg) {
            return value;
        }
    }FnCompose.java:
    package com.demo.ifn;

    import java.io.IOException;
    import java.io.Serializable;

    public class FnCompose implements IFn, Serializable {
        private IFn f1, f2;

        public FnCompose(IFn f1, IFn f2) {
            this.f1 = f1;
            this.f2 = f2;
        }

        public Object invokeCall(Object arg) throws IOException {
            return f2.invokeCall(f1.invokeCall(arg));
        }
    }TestDemo.java:
    package com.demo.ifn;

    public class TestDemo {
        //測試拓撲排序的正確性        private String test;

        public String pMethod(String arg){
            String vul = cMethod(arg);
            return vul;
        }

        public String cMethod(String arg){
            return arg.toUpperCase();
        }
    }AbstractTableModel.java:
    package com.demo.model;

    import com.demo.ifn.IFn;

    import java.io.IOException;
    import java.io.Serializable;
    import java.util.HashMap;

    public class AbstractTableModel implements Serializable {
        private HashMap<String, IFn> __clojureFnMap;

        public AbstractTableModel(HashMap<String, IFn> clojureFnMap) {
            this.__clojureFnMap = clojureFnMap;
        }

        public int hashCode() {
            IFn f = __clojureFnMap.get("hashCode");
            try {
                f.invokeCall(this);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return this.__clojureFnMap.hashCode() + 1;
        }
    }

:下面截圖中數據的順序作了調換,同時數據也只給出com/demo中的數據

Step1 枚舉所有類及每一個類全部方法

classes.dat:

95.png

methods.dat:

96.png

Step2 生成passthrough數據流

passthrough.dat:

97.png

能夠看到IFn的子類中只有FnConstant的invokeCall在passthrough數據流中,由於其餘幾個在靜態分析中沒法判斷返回值與參數的關係。同時TestDemo的cMethod與pMethod都在passthrough數據流中,這也說明了拓撲排序那一步的必要性和正確性。

Step3 枚舉passthrough調用圖

callgraph.dat:

98.png

Step4 搜索可用的source

sources.dat:

99.png

Step5 搜索生成調用鏈

在gadget-chains.txt中找到了以下鏈:

com/demo/model/AbstractTableModel.hashCode()I (0)
  com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (1)
  java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)

能夠看到選擇的確實是找了一條最短的路徑,並無通過FnCompose、FnConstant路徑。

環路形成路徑爆炸

上面流程分析第五步中說到,若是去掉已訪問過節點的判斷會怎麼樣呢,能不能生成通過FnCompose、FnConstant的調用鏈呢?

100.png

陷入了爆炸狀態,Search space無限增長,其中一定存在環路。做者使用的策略是訪問過的節點就再也不訪問了,這樣解決的環路問題,可是丟失了其餘鏈。

好比上面的FnCompose類:

public class Fncompose implements IFn{
    private IFn f1,f2;
    public Object invoke(Object arg){
        return f2.invoke(f1.invoke(arg));
    }}

因爲IFn是接口,因此在調用鏈生成中會查找是它的子類,假如f1,f2都是FnCompose類的對象,這樣造成了環路。

隱式調用

測試隱式調用看工具可否發現,將FnEval.java作一些修改:

FnEval.java    package com.demo.ifn;

    import java.io.IOException;
    import java.io.Serializable;

    public class FnEval implements IFn, Serializable {
        private String cmd;

        public FnEval() {
        }

        @Override        public String toString() {
            try {
                Runtime.getRuntime().exec(this.cmd);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return "FnEval{}";
        }

        public Object invokeCall(Object arg) throws IOException {
            this.cmd = (String) arg;
            return this + " test";
        }
    }

結果:

com/demo/model/AbstractTableModel.hashCode()I (0)
  com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (0)
  java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lang/StringBuilder; (1)
  java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; (0)
  com/demo/ifn/FnEval.toString()Ljava/lang/String; (0)
  java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)

隱式調用了tostring方法,說明在字節碼分析中作了查找隱式調用這一步。

不遵循反射調用

在github的工具說明中,做者也說到了在靜態分析中這個工具的盲點,像下面這中FnEval.class.getMethod("exec", String.class).invoke(null, arg)寫法是不遵循反射調用的,將FnEval.java修改:

FnEval.java    package com.demo.ifn;import java.io.IOException;import java.io.Serializable;import java.lang.reflect.InvocationTargetException;public class FnEval implements IFn, Serializable {

    public FnEval() {
    }

    public static void exec(String arg) throws IOException {
        Runtime.getRuntime().exec(arg);
    }

    public Object invokeCall(Object arg) throws IOException {
        try {
            return FnEval.class.getMethod("exec", String.class).invoke(null, arg);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }}

通過測試,確實沒有發現。可是將FnEval.class.getMethod("exec", String.class).invoke(null, arg)改成this.getClass().getMethod("exec", String.class).invoke(null, arg)這種寫法倒是能夠發現的。

特殊語法

測試一下比較特殊的語法呢,好比lambda語法?將FnEval.java作一些修改:

FnEval.java:
    package com.demo.ifn;

    import java.io.IOException;
    import java.io.Serializable;

    public class FnEval implements IFn, Serializable {

        public FnEval() {
        }

        interface ExecCmd {
            public Object exec(String cmd) throws IOException;
        }

        public Object invokeCall(Object arg) throws IOException {
            ExecCmd execCmd = cmd -> {
                return Runtime.getRuntime().exec(cmd);
            };
            return execCmd.exec((String) arg);
        }
    }

通過測試,沒有檢測到這條利用鏈。說明目前語法分析那一塊尚未對特殊語法分析。

匿名內部類

測試匿名內部類,將FnEval.java作一些修改:

FnEval.java:
    package com.demo.ifn;

    import java.io.IOException;
    import java.io.Serializable;

    public class FnEval implements IFn, Serializable {

        public FnEval() {
        }

        interface ExecCmd {
            public Object exec(String cmd) throws IOException;
        }

        public Object callExec(ExecCmd execCmd, String cmd) throws IOException {
            return execCmd.exec(cmd);
        }

        public Object invokeCall(Object arg) throws IOException {
            return callExec(new ExecCmd() {
                @Override                public Object exec(String cmd) throws IOException {
                    return Runtime.getRuntime().exec(cmd);
                }
            }, (String) arg);
        }
    }

通過測試,沒有檢測到這條利用鏈。說明目前語法分析那一塊尚未對匿名內部類的分析。

sink->source?

既然能source->sink,那麼能不能sink->source呢?由於搜索source->sink時,source和sink都是已知的,若是搜索sink->source時,sink與soure也是已知的,那麼source->sink與sink->source好像沒有什麼區別?若是能將source總結爲參數可控的一類特徵,那麼sink->source這種方式是一種很是好的方式,不只能用在反序列化漏洞中,還能用在其餘漏洞中(例如模板注入)。可是這裏也還有一些問題,好比反序列化是將this以及類的屬性都看成了0參,由於反序列化時這些都是可控的,可是在其餘漏洞中這些就不必定可控了。

目前還不知道具體如何實現以及會有哪些問題,暫時先不寫。

缺陷

目前尚未作過大量測試,只是從宏觀層面分析了這個工具的大體原理。結合平安集團分析文章以及上面的測試目前能夠總結出一下幾個缺點(不止這些缺陷):

  • callgraph生成不完整

  • 調用鏈搜索結果不完整,這是因爲查找策略致使的

  • 一些特殊語法、匿名內部類還不支持

  • ...

設想與改進

  • 對以上幾個缺陷進行改進

  • 結合已知的利用鏈(如ysoserial等)不斷測試

  • 儘量列出全部鏈並結合人工篩選判斷,而做者使用的策略是隻要通過這個節點有一條鏈,其餘鏈通過這個節點時就再也不繼續尋找下去。主要解決的就是最後那個調用鏈環路問題,目前看到幾種方式:

  • DFS+最大深度限制

  • 繼續使用BFS,人工檢查生成的調用鏈,把無效的callgraph去掉,重複運行

  • 調用鏈緩存(這一個暫時還沒明白具體怎麼解決環路的,只是看到了這個方法)

個人想法是在每條鏈中維持一個黑名單,每次都檢查是否出現了環路,若是在這條鏈中出現了環路,將形成環路的節點加入黑名單,繼續使其走下去。固然雖然沒有了環,也能會出現路徑無限增加的狀況,因此仍是須要加入路徑長度限制。

  • 嘗試sink->source的實現

  • 多線程同時搜索多條利用鏈加快速度

  • ...

最後

在原理分析的時候,忽略了字節碼分析的細節,有的地方只是暫時猜想與測試得出的結果,因此可能存在一些錯誤。字節碼分析那一塊是很重要的一環,它對污點的判斷、污點的傳遞調用等起着很重要的做用,若是這些部分出現了問題,整個搜索過程就會出現問題。因爲ASM框架對使用人員要求較高,因此須要要掌握JVM相關的知識才能較好使用ASM框架,因此接下來的就是開始學習JVM相關的東西。這篇文章只是從宏觀層面分析這個工具的原理,也算是給本身增長些信心,至少明白這個工具不是沒法理解和沒法改進的,同時後面再接觸這個工具進行改進時也會間隔一段時間,回顧起來也方便,其餘人若是對這個工具感興趣也能夠參考。等之後熟悉並能操縱Java字節碼了,在回頭來更新這篇文章並改正可能有錯誤的地方。

若是這些設想與改進真的實現而且進行了驗證,那麼這個工具真的是一個得力幫手。可是這些東西要實現還有較長的一段路要走,還沒開始實現就預想到了那麼多問題,在實現的時候會遇到更多問題。不過好在有一個大體的方向了,接下來就是對各個環節逐一解決了。

參考

相關文章
相關標籤/搜索