想法早在几个月之前就有了,月初收好友之邀请,夜游鼓浪屿,彼时夜朗星稀,山海一色,偶有微波抚足,不觉间有了点写东西的感觉,晚上回到旅社简单写了下。等回到北京后,不料润色之意全无,就凑合看吧。
0x01 内存马简历史 其实内存马由来已久,早在17年n1nty师傅的《Tomcat源码调试笔记-看不见的shell》 中已初见端倪,但一直不温不火。后经过rebeyong师傅使用agent技术 加持后,拓展了内存马的使用场景,然终停留在奇技淫巧上。在各类hw洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。特别是今年在shiro的回显研究之后,引发了无数安全研究员对内存webshell的研究,其中涌现出了LandGrey师傅构造的Spring controller内存马 。至此内存马开枝散叶发展出了三大类型:
servlet-api类
spring类
Java Instrumentation类
内存马这坛深巷佳酒,一时间流行于市井与弄堂之间。上至安全研究员下至普通客户,人尽皆知。正值hw来临之际,不难推测届时必将是内存马横行天下之日。而各大安全厂商却迟迟未见动静。所谓表面风平浪静,实则暗流涌动。或许一场内存马的围剿计划正慢慢展开。作为攻击方向的研究人员,没有对手就制造对手,攻防互换才能提升内存马技术的发展。
0x02 查杀思路 我们判断逻辑很朴实,利用Java Agent技术遍历所有已经加载到内存中的class。先判断是否是内存马,是则进入内存查杀。
1 2 3 4 5 6 7 8 9 10 11 12 public class Transformer implements ClassFileTransformer { public byte [] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte [] bytes) throws IllegalClassFormatException { if (isMemshell(aClass,bytes)){ byte [] newClassByte = killMemshell(aClass,bytes); return newClassByte; }else { return bytes; } } }
0x03 内存马的识别 要识别,我们就需要细思内存马有什么特征。下面列下我思考过的检查点。
filter名字很特别
内存马的Filter名一般比较特别,有shell
或者随机数等关键字。这个特征稍弱,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常的名字。
filter优先级是第一位
为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。
对比web.xml中没有filter配置
内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter
标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。
特殊classloader加载
我们都知道Filter也是class,也是必定有特定的classloader加载。一般来说,正常的Filter都是由中间件的WebappClassLoader加载的。反序列化漏洞喜欢利用TemplatesImpl和bcel执行任意代码。所以这些class往往就是以下这两个:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader
com.sun.org.apache.bcel.internal.util.ClassLoader
这个特征是一个特别可疑的点了。当然了,有的内存马还是比较狡猾的,它会注入class到当前线程中,然后实例化注入内存马。这个时候内存马就有可能不是上面两个classloader。
对应的classloader路径下没有class文件
所谓内存马就是代码驻留内存中,本地无对应的class文件。所以我们只要检测Filter对应的ClassLoader目录下是否存在class文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static boolean classFileIsExists (Class clazz) { if (clazz == null ){ return false ; } String className = clazz.getName(); String classNamePath = className.replace("." , "/" ) + ".class" ; URL is = clazz.getClassLoader().getResource(classNamePath); if (is == null ){ return false ; }else { return true ; } }
Filter的doFilter方法中有恶意代码
我们可以把内存中所有的Filter的class dump出来,使用fernflower
等反编译工具分析看看,是否存在恶意代码,比如调用了如下可疑的方法:
java.lang.Runtime.getRuntime
defineClass
invoke
…
不难分析,内存马的命门在于5
和6
。简单说就是Filter型内存马首先是一个Filter类,同时它在硬盘上没有对应的class文件。若dump出的class还有恶意代码,那是内存马无疑啦。大致检查的代码如下:
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 private static boolean isMemshell (Class targetClass,byte [] targetClassByte) { ClassLoader classLoader = null ; if (targetClass.getClassLoader() != null ) { classLoader = targetClass.getClassLoader(); }else { classLoader = Thread.currentThread().getContextClassLoader(); } Class clsFilter = null ; try { clsFilter = classLoader.loadClass("javax.servlet.Filter" ); }catch (Exception e){ } if (clsFilter != null && clsFilter.isAssignableFrom(targetClass)){ if (classLoader.getClass().getName().contains("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader" ) || classLoader.getClass().getName().contains("com.sun.org.apache.bcel.internal.util.ClassLoader" )){ return true ; } if (classFileIsExists(targetClass)){ return true ; } String[] blacklist = new String[]{"getRuntime" ,"defineClass" ,"invoke" }; String clsJavaCode = FernflowerUtils.decomper(targetClass,targetClassByte); for (String b:blacklist){ if (clsJavaCode.contains(b)){ return true ; } } }else { return false ; } return false ; }
PS: 本文讨论查杀的思路,给出的代码只是概念正面的伪装代码。完美的方案是将以上6点作为判断指标,并根据指标的重要性赋予不同权重。满足的条件越多越可能是内存马。
0x04 内存马的查杀 内存马识别完成,接下来就是如何查杀了。
方法一: 清除内存马中的Filter的恶意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static byte [] killMemshell(Class clsMemshell,byte [] byteMemshell) throws Exception{ File file = new File(String.format("/tmp/%s.class" ,clsMemshell.getName())); if (file.exists()){ file.delete(); } FileOutputStream fos = new FileOutputStream(file.getAbsoluteFile()); fos.write(byteMemshell); fos.flush(); fos.close(); ClassPool cp = ClassPool.getDefault(); cp.insertClassPath("/tmp/" ); CtClass cc = cp.getCtClass(clsMemshell.getName()); CtMethod m = cc.getDeclaredMethod("doFilter" ); m.addLocalVariable("elapsedTime" , CtClass.longType); m.setBody("{$2.getWriter().write(\"Your memory horse has been killed by c0ny1\");}" ); byte [] byteCode = cc.toBytecode(); cc.detach(); return byteCode; }
方法二: 模拟中间件注销Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Object standardContext = ...; Field _filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs" ); _filterConfigs.setAccessible(true ); Object filterConfigs = _filterConfigs.get(standardContext); Map<String, ApplicationFilterConfig> filterConfigMap = (Map<String, ApplicationFilterConfig>)filterConfigs; for (Map.Entry<String, ApplicationFilterConfig> map : filterConfigMap.entrySet()){ String filterName = map.getKey(); ApplicationFilterConfig filterConfig = map.getValue(); Filter filterObject = filterConfig.getFilter(); if (filterName.startsWith("memshell" )){ SecurityUtil.remove(filterObject); filterConfigMap.remove(filterName); } }
两种方法各有优劣,第一种方法比较通用,直接适配所有中间件。但恶意Filter依然在,只是恶意代码被清除了。第二种方法比较优雅,恶意Filter会被清除掉。但每种中间件注销Filter的逻辑不尽相同,需要一一适配。为了方便演示我们选第一种。
0x05 demo展示 最后给大家展示下,我查杀demo的效果。
0x06 总结 本文我们对Filter型内存马的识别与查杀做了细致的分析,其实Servlet型,拦截器型和Controller型的查杀方法也是万变不离其中,可如法炮制。但这样的思路无法查杀Agent型内存马,Agent型内存马查杀难点在“查”不在“杀”,具体的难点在那,又是如何解决呢?我会在后续的《查杀Java web Agent型内存马》中继续分享我的思考。
0x07 参考文章