查杀Java web filter型内存马

想法早在几个月之前就有了,月初收好友之邀请,夜游鼓浪屿,彼时夜朗星稀,山海一色,偶有微波抚足,不觉间有了点写东西的感觉,晚上回到旅社简单写了下。等回到北京后,不料润色之意全无,就凑合看吧。

0x01 内存马简历史

其实内存马由来已久,早在17年n1nty师傅的《Tomcat源码调试笔记-看不见的shell》中已初见端倪,但一直不温不火。后经过rebeyong师傅使用agent技术加持后,拓展了内存马的使用场景,然终停留在奇技淫巧上。在各类hw洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。特别是今年在shiro的回显研究之后,引发了无数安全研究员对内存webshell的研究,其中涌现出了LandGrey师傅构造的Spring controller内存马。至此内存马开枝散叶发展出了三大类型:

  1. servlet-api类
    • filter型
    • servlet型
  2. spring类
    • 拦截器
    • controller型
  3. Java Instrumentation类
    • agent型

内存马这坛深巷佳酒,一时间流行于市井与弄堂之间。上至安全研究员下至普通客户,人尽皆知。正值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 内存马的识别

要识别,我们就需要细思内存马有什么特征。下面列下我思考过的检查点。

  1. filter名字很特别

内存马的Filter名一般比较特别,有shell或者随机数等关键字。这个特征稍弱,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常的名字。

  1. filter优先级是第一位

为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。

  1. 对比web.xml中没有filter配置

内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。

  1. 特殊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。

  1. 对应的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;
}
}
  1. Filter的doFilter方法中有恶意代码

我们可以把内存中所有的Filter的class dump出来,使用fernflower等反编译工具分析看看,是否存在恶意代码,比如调用了如下可疑的方法:

  • java.lang.Runtime.getRuntime
  • defineClass
  • invoke

不难分析,内存马的命门在于56。简单说就是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){
}

// 是否是filter
if(clsFilter != null && clsFilter.isAssignableFrom(targetClass)){
// class loader 是不是Templates或bcel
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;
}

// 是否存在ClassLoader的文件目录下存在对应的class文件
if(classFileIsExists(targetClass)){
return true;
}

// filter是否包含恶意代码。
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("{$3.doFilter($1,$2);}");
// 方便演示代码:
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
//反序列化执行代码反射获取到StandardContext
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();
// 如果是内存马的filter名
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 参考文章