Java安全之反射
前言
關于Java安全,反序列化漏洞一直是一個熱門話題,而反序列化漏洞?可以從反射開始說起。通過反射,對象可以通過反射獲取他的類,類可以通過反射拿到所有?法(包括私有),拿到的?法可以調?,總之通過“反射”,我們可以將Java這種靜態語?附加上動態特性。
入門
有以下三種方法獲取?個“類”,也就是java.lang.Class對象:
//1、通過對象調用 getClass() 方法來獲取,通常應用在:比如你傳過來一個 Object類型的對象,而我不知道你具體是什么類,用這種方法
Person p1 = new Person();
Class c1 = p1.getClass();
//2、直接通過類名.class 的方式得到,該方法最為安全可靠,程序性能更高這說明任何一個類都有一個隱含的靜態成員變量 class
Class c2 = Person.class;
//3、通過 Class 對象的 forName() 靜態方法來獲取,用的最多,但可能拋ClassNotFoundException異常
Class c3 = Class.forName("com.ys.reflex.Person");
在安全研究里邊,我們常見的各種payload幾乎都是使用Class.forName方法來獲取類
forName有兩個函數重載:
- Class forName(String name)
- Class forName(String name, boolean initialize, ClassLoader loader)


第?個就是我們最常?的獲取class的?式,通過類名來獲取,其實可以理解為第?種?式的?個封裝。
Class.forName(className) // 等于 Class.forName(className, true, currentLoader)
ClassLoader 是什么呢?它就是?個“加載器”,告訴Java虛擬機如何加載這個類。這是另外的一個知識點。Java默認的ClassLoader就是根據類名來加載類,這個類名是類完整路徑,如java.lang.Runtime
關于第二個參數initialize,我們可以將這個“初始化”理解為類的初始化,我們執行下面這行代碼,JVM會做什么呢?
Person p = new Person("zhangsan",20);
- 因為new用到了Person.class.所以會先找到Person.class文件并加載到內存中。 2
- 執行該類中的
static代碼塊,如果有的話,給Person.class類進行初始化。 - 在堆內存中開辟空間,分配內存地址。
- 在堆內存中建立對象的特有屬性。并進行
默認初始化。 - 對屬性進行顯示初始化。
- 對對象進行構造代碼塊初始化。
- 對對象進行對應的構造函數初始化。
- 將內存地址付給棧內存中的p變量
可以分為三類初始化:static代碼塊的初始化,其他初始化,構造函數初始化,其中構造函數初始化會涉及到super類的構造函數初始化,這里不細講了。
需要注意的是使用class.forName()會對類的靜態代碼塊進行初始化(不會初始化類的構造函數),
那么我們就可以編寫?個惡意類,將惡意代碼放置在static {}中,從?執?
import java.lang.Runtime;
import java.lang.Process;
public class command {
static {
try {
Runtime rt = Runtime.getRuntime();
String commands = "calc.exe";
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
}
}
}

進階
上面可以看到,我們是先import java.lang.Runtime,然后再使用;正常情況下,除了jdk內置類,如果我們想拿到一個類,需要先import才能使用。而使用forName就不需要,這樣我們可以加載任意類進行攻擊。
獲得類以后,我們可以繼續使用反射來獲取這個類中的屬性、方法,也可以實例化這個類,并調用方法
在反射類庫中,用于實例化對象的方法有兩個。
Class.newInstance():這個方法只需要提供一個class實例就可以實例化對象,如這個方法不支持任何入參,底層是依賴無參數的構造器Constructor進行實例化的。Constructor.newInstance(Object...init args):這個方法需要提供java.lang.reflect.Constructor<T>實例和一個可變參數數組進行對象的實例化,這個方法除了可以傳入構造參數之外,還有一個好處就是可以通過抑制修飾符訪問權限檢查,也就是私有的構造器也可以用于實例化對象。
我們在構造payload的時候,實例化不成功的原因有以下兩個:
- 使用的類沒有無參構造函數,因為
newInstance()底層是依賴無參數的構造器實現的,沒有無參構造函數,怎么可能實例化成功 - 使用的類構造函數是私有的,我們可以使用
Constructor.newInstance(Object...initargs)來實例化
我們來分析下面這個payload:
Class cls = Class.forName("java.lang.Runtime");
Method execMethod = cls.getMethod("exec", String.class);
Method getRuntimeMethod = cls.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(cls);
execMethod.invoke(runtime, "calc.exe");
首先獲取java.lang.Runtime類,接下來獲取這個類的exec方法,接下來又獲取getRuntime方法,下面兩部可能看著有點疑惑了,先來看invoke是干嘛的
invoke 的作用是執行方法,它的第一個參數是:
- 如果這個方法是一個普通方法,那么第一個參數是類對象
- 如果這個方法是一個靜態方法,那么第一個參數是類
- static修飾的靜態方法會隨著類的定義而被分配和裝載入內存中
- 普通方法只有在對象創建時,在對象的內存中才有這個方法的代碼段
這也就是為什么invoke方法參數不同的原因所在。常規的方法執行是: Person per = Person.eat(1,2,3)
反射則是 eat.invoke(Persno,1,2,3)
有以下代碼段:
Class cls = Class.forName("java.lang.Runtime"); //獲取類
Method execMethod =cls.getMethod("exec",String.class); //獲取方法
execMethod.invoke(cls.newInstance(), "calc.exe"); //實例化類并執行方法

按理說,應該彈計算器啊,為什么報錯了呢?看報錯提示,不能獲取一個被“privite”修飾符修飾的類。

原來構造方法是私有的,那肯定是實例化不了的,為什么構造方法要搞成私有的,不想讓人用?
這其中就涉及到一個常見的設計模式--->工廠模式,具體是什么,就不說了。舉例
我們在做Web開發的時候,數據庫連接只需要建立一次,而不是每次用到數據庫的時候再新建立一個連 接,此時作為開發者你就可以將數據庫連接使用的類的構造函數設置為私有,然后編寫一個靜態方法來 獲取:
public class DBC {
private static DBC instance = new DBC();
public static DBC getInstance() {
return instance;
}
private DBC() {
// 建立連接的代碼...
}
}
只有只有類初始化的時候會執行一次構造函數,后面只能通過getInstance獲取這個對象,避免建立多個數據庫連接。
而Runtime類就是單例模式,我們只能通過Runtime.getRuntime()來獲取到Runtime對 象。我們將上述Payload進行修改即可正常執行命令了
public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.Runtime"); //獲取類
Method execMethod =cls.getMethod("exec",String.class); //獲取exec方法
Method getRuntimeMethod = cls.getMethod("getRuntime"); //獲取getRuntime方法
Object runtime = getRuntimeMethod.invoke(cls); //執行getRuntime方法來獲取Runtime類
execMethod.invoke(runtime, "calc.exe"); //執行方法exec方法
}
}
這樣就和一開始的payload對應上了。

深入
兩個問題:
- 如果一個類沒有無參構造方法,也沒有類似單例模式里的靜態方法,我們怎樣通過反射實例化該類呢?
- 如果一個方法或構造方法是私有方法,我們是否能執行它呢?
一
第一個問題:我們上節在開始就說過用于實例化對象的方法有兩個,我們只說了第一種,而第二種方法就可以解決這節第一個問題。
首先我們需要通過反射方法getConstructor()獲得獲得一個Constructor對象,
和 getMethod 類似, getConstructor 接收的參數是構造函數列表類型,因為構造函數也支持重載, 所以必須用參數列表類型才能唯一確定一個構造函數,聽著有點繞,看以下例子:
Class cl=Class.forName(Person);
//獲取到Person(String name,int age) 構造函數
Constructor con=cl.getConstructor(String.class,int.class);
//通過構造器對象 newInstance 方法對對象進行初始化,使用有參數構造函數
Object obj=con.newInstance("神奇的我",12);
我們常用的另一種命令執行的方法ProcessBuilder.start(),我們使用反射來獲取其構造函數,然后調用start()來執行命令:
public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.ProcessBuilder"); //獲取類
Constructor con = cls.getConstructor(List.class); // 獲取構造器
ProcessBuilder process = (ProcessBuilder) con.newInstance(Arrays.asList("calc.exe")); //通過構造器實例化對象
process.start();
}
}

這兒可能有點疑惑了,怎么start就直接彈計算器了?
ProcessBuilder有兩個構造函數:
public ProcessBuilder(List<String> command) public ProcessBuilder(String... command)
我們用的是第一個形式的,所以在getConstructor的時候傳入的是List.class
所以接下來需要用數組的形式傳入calc.exec參數
這里需要注意,我們通過Constructor實例化對象返回的是一個Object對象,我這里是u強制類型轉換,有時候我們利用漏洞的時候(在表達式上下文中)是沒有這種語法的。所以,我們不能直接執行命令,仍需利用反射來完成這一步,payload如下:
public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.ProcessBuilder"); //獲取類
Constructor con = cls.getConstructor(List.class); // 獲取構造器
Method startMethod = cls.getMethod("start"); //獲取start方法
Object probulid = con.newInstance(Arrays.asList("calc.exe")); //通過構造器實例化對象
startMethod.invoke(probulid);
}
}
通過getMethod("start")獲取到start方法,然后invoke執行,invoke的參數就是ProcessBuilder Object了。

如果我們要使用public ProcessBuilder(String... command)這個構造函數,具體的paayload該如何構造呢?
這又涉及到Java里的可變長參數(avarargs)了。正如其他語言一樣,Java也支持可變長參數,就是當你 定義函數的時候不確定參數數量的時候,可以使用...這樣的語法來表示“這個函數的參數個數是可變的”。 對于可變長參數,Java其實在編譯的時候會編譯成一個數組,也就是說,下面這兩種寫法在底層是等價的:
public void hello(String[] names) {}
public void hello(String...names) {}
也由此,如果我們有一個數組,想傳給say函數,只需直接傳即可
String[] names = {"hello", "world"};
hello(names);
對于反射來說,如果要獲取的目標函數里包含可變長參數,其實我們認為它是數組就行了。 所以,我們將字符串數組的類String[].class傳給getConstructor,獲取ProcessBuilder的第二種構造函數:
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)
在調用 newInstance 的時候,因為這個函數本身接收的是一個可變長參數,我們傳給 ProcessBuilder 的也是一個可變長參數,二者疊加為一個二維數組,所以整個Payload如下:
public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.ProcessBuilder");
Constructor con = cls.getConstructor(String[].class);
Method startMethod = cls.getMethod("start");
Object probuild =con.newInstance(new String[][]{{"calc.exe"}});
startMethod.invoke(probuild);
}
}

那為什么在newInstance時傳入的是一個二維數組呢?
這是因為在newInstance函數本身接收的是一個可變長參數,我們傳給ProcessBuilder也是一個可變長參數,二者疊加為一個二維數組。這兒比較繞,得轉過彎來
二
如果一個方法或構造方法是私有方法,我們是否能執行它呢?
答案是可以。通過getDeclared系列方法
getMethod系列方法獲取的是當前類中所有公共方法,包括從父類繼承的方法getDeclaredMethod系列方法獲取的是當前類中“聲明”的方法,是實在寫在這個類里的,包括- 私有的方法,但不能獲取父類繼承的方法
getDeclaredMethod的具體用法和getMethod類似,getDeclaredConstructor的具體體用法和getConstructor類似
前面我們說過Runtime這個類的構造函數是私有的,我們需要用Runtime.getRuntime()來 獲取對象。其實現在我們也可以直接用getDeclaredConstructor來獲取這個私有的構造方法來實例化對象,進而執行命令:
public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.Runtime");
Constructor con = cls.getDeclaredConstructor();
con.setAccessible(true);
cls.getMethod("exec", String.class).invoke(con.newInstance(), "calc.exe");
}
}

可見,這里使用了一個方法setAccessible,這個是必須的。我們在獲取到一個私有方法后,必須用setAccessible修改它的作用域,否則仍然不能調用。