[备份]基于污点分析的JSP Webshell检测

0x00 前言

在11月初,我做了一些JSP Webshell的免杀研究,主要参考了三梦师傅开源的代码。然后加入了一些代码混淆手段,编写了一个免杀马生成器JSPHorse,没想到在Github上已收获500+的Star

做安全只懂攻击不够,还应该懂防御

之前只做了一些免杀方面的事情,欠缺了防御方面的思考

于是我尝试自己做一个JSP Webshell的检测工具,主要原理是ASM做字节码分析并模拟执行,分析栈帧(JVM Stack Frame)得到结果

只输入一个JSP文件即可进行这一系列的分析,大致需要以下四步

img

类似之前写的工具CodeInspector,不过它是半成品只能理论上的学习研究,而这个工具是可以落地进行实际的检测,下面给大家展示下检测效果

 

0x01 效果

时间原因只做了针对于反射型JSP Webshell的检测

效果还是不错的,各种变形都可以轻松检测出

来个基本的反射马:1.jsp

查出是Webshell

img

如果把字符串给拆出来:2.jsp

查出是Webshell

img

进一步变化,拆开字符串:3.jsp

或者合并成一行

都可以查出是Webshell

img

如果是正常逻辑,和执行命令无关:4.jsp

那么不会存在误报

img

 

0x03 JSP处理

第一步我们需要把输入的JSP转为Java代码,之所以这样做因为JSP无法直接变成字节码

原理其实简单:造一个模板类,把JSP<% xxx %>中的xxx填入模板

模板如下,简单取了三个JSP中常用的变量放入参数

简单做了一下解析,可能会存在BUG但在当前的情景下完全够用

上面代码有一处坑:想从打包后的JarResource里读东西必须用getResourceAsStream,如果用URI的方式会报错。另外这里用Main.class.getClassLoader()是为了读到classes根目录

经过处理后JSP变成这样的代码,可以使用Javac命令手动编译

 

0x04 动态编译

手动编译的时候其实有一个坑:系统不包含servlet相关的库,所以会报错

这个好解决,只需要一个参数javac Webshell.java -cp javax.servlet-api.jar

在网上查了下如何动态编译,这个代码还是比较多的

但都没有设置参数,我们情况特殊需要classpath参数,最终看官方文档得到了答案

通过以上的代码会得到一个Webshell.class的字节码文件,这就是我们真正需要的东西

这里同样有一个坑:ToolProvider.getSystemJavaCompiler()这句话在java -jar xxx.jara的情况下是空指针,通过查询解决办法,发现需要在JDK/JRElib加入tools.jar并且将环境变量配到JDK/bin而不是JDK/JRE/binJRE/bin

当我们动态编译Webshell.javaWebshell.class后,读取字节码到内存中,就可以删除这两个临时文件了

 

0x05 模拟栈帧

JVM在每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁

而每个Frame的结构如下,主要由本地变量数组(local variables)和操作栈(operand stack)组成

局部变量表所需的容量大小是在编译期确定下来的,表中的变量只在当前方法调用中有效

JVM把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈

参考我在Github代码,该类构造了Operand StackLocal Variables Array并模拟操作

在用ASM技术解析class文件的时候,模拟他们在JVM中执行的过程,实现数据流分析

img

使用代码模拟两大数据结构

在进入方法的时候,JVM会初始化这两大数据结构

在方法执行的时候,对这两种数据结构进行POP/PUSH等操作,随便选了其中一部分供参考

为什么能够这样操作,参考Oracle的JVM指令文档:官方文档

上文其实略枯燥,接下来结合实例和大家画图分析,这将会一目了然

 

0x06 检测实现

新建一个ClassVisitor用于分析字节码,以下这三部是ASM规定的分析字节码方式

大家需要注意ASM是观察者模式,需要理解阻断传递的思想

其实ReflectionShellClassVisitor不是重点,因为我们的JSP Webshell逻辑都写在Webshell.invoke方法中,所以检测逻辑在ReflectionShellMethodAdapter类中

重点放在ReflectionShellMethodAdapter

首先我们要确认可控参数,也就是污点分析里的Source,不难得出来自于request.getParameter

这一步的字节码如下

这四步过程如下:

img

我们可以在INVOKEINTERFACE的时候编写如下代码

接下来看反射的第一句Class.forName("java.lang.Runtime")

由于调用STATIC方法不需要this然后返回值保存在局部变量表第5位

img

这里我给反射三步的LDC分别给上自己的flag做跟踪

注意到LDC命令执行完后保存至栈顶

下一句rt.getMethod("getRuntime")稍微复杂

中间主要是多了一步ANEWARRAY操作

img

这个染成黄色的过程在代码中如下

下一步是rt.getMethod("exec", String.class)和上面几乎一致,不过数组里添加了元素

这一步几乎重复,就不再画图了,可以看出最后保存到局部变量表第7位

其中陌生的命令有DUPAASTORE两个,暂不分析,我们在method.invoke中细说

代码中的处理类似

接下来该最关键的一行了:ex.invoke(gr.invoke(null), cmd)

第一步的INVOKEVIRTUAL只是得到了Runtime对象

第二步的INVOKEVIRTUAL才是exec(obj,cmd)执行命令的代码

所以我们重点从第二步分析

AASTORE之前的过程如下(防止干扰栈中存在的其他元素没有画出)

img

AASTOREINVOKE的过程如下(之前在栈中没有画出的元素都补充到)

img

注意其中的细节

所以我们需要手动处理下AASTORE情况以便于让参数传递下去

至于最后一步的判断就很简单了

其实栈中第2个元素也可以判断下,我简化了一些不必要的操作

 

0x07 总结

后续考虑加入其他的一些检测,师傅们可以试试Bypass手段哈哈